From 3fc394921e2650910c43984ad1390c99f62073ad Mon Sep 17 00:00:00 2001 From: 2910410219 <2910410219@qq.com> Date: Fri, 12 Jun 2026 17:23:00 +0800 Subject: [PATCH] Initial commit: AI chat assistant with workflow chat, workspace, and profile tabs Co-Authored-By: Claude --- .gitignore | 4 + App.uvue | 70 +++++ CLAUDE.md | 67 +++++ components/AppDrawer.uvue | 208 +++++++++++++++ components/BottomTabBar.uvue | 93 +++++++ components/DrawerHeader.uvue | 57 +++++ components/HistoryItem.uvue | 149 +++++++++++ components/HistoryList.uvue | 85 +++++++ components/InputArea.uvue | 126 +++++++++ components/MessageBubble.uvue | 288 +++++++++++++++++++++ components/MessageList.uvue | 175 +++++++++++++ components/ProfilePanel.uvue | 251 ++++++++++++++++++ components/TopBar.uvue | 129 ++++++++++ components/TypingIndicator.uvue | 53 ++++ components/WorkflowChip.uvue | 65 +++++ components/WorkflowSelector.uvue | 67 +++++ components/WorkspaceItem.uvue | 132 ++++++++++ components/WorkspaceView.uvue | 81 ++++++ composables/useConversation.uts | 259 +++++++++++++++++++ composables/useConversations.uts | 93 +++++++ composables/useDrawer.uts | 29 +++ composables/useStorage.uts | 53 ++++ composables/useWorkflow.uts | 43 ++++ composables/useWorkspace.uts | 69 +++++ constants/index.uts | 65 +++++ index.html | 20 ++ main.uts | 9 + manifest.json | 43 ++++ pages.json | 19 ++ pages/index/index.uvue | 425 +++++++++++++++++++++++++++++++ platformConfig.json | 7 + static/logo.png | Bin 0 -> 4023 bytes types/index.uts | 59 +++++ uni.scss | 156 ++++++++++++ 34 files changed, 3449 insertions(+) create mode 100644 .gitignore create mode 100644 App.uvue create mode 100644 CLAUDE.md create mode 100644 components/AppDrawer.uvue create mode 100644 components/BottomTabBar.uvue create mode 100644 components/DrawerHeader.uvue create mode 100644 components/HistoryItem.uvue create mode 100644 components/HistoryList.uvue create mode 100644 components/InputArea.uvue create mode 100644 components/MessageBubble.uvue create mode 100644 components/MessageList.uvue create mode 100644 components/ProfilePanel.uvue create mode 100644 components/TopBar.uvue create mode 100644 components/TypingIndicator.uvue create mode 100644 components/WorkflowChip.uvue create mode 100644 components/WorkflowSelector.uvue create mode 100644 components/WorkspaceItem.uvue create mode 100644 components/WorkspaceView.uvue create mode 100644 composables/useConversation.uts create mode 100644 composables/useConversations.uts create mode 100644 composables/useDrawer.uts create mode 100644 composables/useStorage.uts create mode 100644 composables/useWorkflow.uts create mode 100644 composables/useWorkspace.uts create mode 100644 constants/index.uts create mode 100644 index.html create mode 100644 main.uts create mode 100644 manifest.json create mode 100644 pages.json create mode 100644 pages/index/index.uvue create mode 100644 platformConfig.json create mode 100644 static/logo.png create mode 100644 types/index.uts create mode 100644 uni.scss diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2370852 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +unpackage/ +.hbuilderx/ +.DS_Store diff --git a/App.uvue b/App.uvue new file mode 100644 index 0000000..da20992 --- /dev/null +++ b/App.uvue @@ -0,0 +1,70 @@ + + + diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..7dde7ea --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,67 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is a **uni-app x** project — DCloud's cross-platform app framework that compiles to native Android, iOS, HarmonyOS, H5, and various mini-programs. The `assistant` directory is the project root. + +- **Language**: UTS (uni-app TypeScript) — a TypeScript superset that compiles to platform-native code +- **UI Framework**: Vue 3 with Composition API (` + + diff --git a/components/BottomTabBar.uvue b/components/BottomTabBar.uvue new file mode 100644 index 0000000..1333994 --- /dev/null +++ b/components/BottomTabBar.uvue @@ -0,0 +1,93 @@ + + + + + diff --git a/components/DrawerHeader.uvue b/components/DrawerHeader.uvue new file mode 100644 index 0000000..ad5c1e3 --- /dev/null +++ b/components/DrawerHeader.uvue @@ -0,0 +1,57 @@ + + + + + diff --git a/components/HistoryItem.uvue b/components/HistoryItem.uvue new file mode 100644 index 0000000..8859cee --- /dev/null +++ b/components/HistoryItem.uvue @@ -0,0 +1,149 @@ + + + + + diff --git a/components/HistoryList.uvue b/components/HistoryList.uvue new file mode 100644 index 0000000..6b028ba --- /dev/null +++ b/components/HistoryList.uvue @@ -0,0 +1,85 @@ + + + + + diff --git a/components/InputArea.uvue b/components/InputArea.uvue new file mode 100644 index 0000000..01f6680 --- /dev/null +++ b/components/InputArea.uvue @@ -0,0 +1,126 @@ + + + + + diff --git a/components/MessageBubble.uvue b/components/MessageBubble.uvue new file mode 100644 index 0000000..4c376d7 --- /dev/null +++ b/components/MessageBubble.uvue @@ -0,0 +1,288 @@ + + + + + diff --git a/components/MessageList.uvue b/components/MessageList.uvue new file mode 100644 index 0000000..da23095 --- /dev/null +++ b/components/MessageList.uvue @@ -0,0 +1,175 @@ + + + + + diff --git a/components/ProfilePanel.uvue b/components/ProfilePanel.uvue new file mode 100644 index 0000000..59f1638 --- /dev/null +++ b/components/ProfilePanel.uvue @@ -0,0 +1,251 @@ + + + + + diff --git a/components/TopBar.uvue b/components/TopBar.uvue new file mode 100644 index 0000000..c3a31a6 --- /dev/null +++ b/components/TopBar.uvue @@ -0,0 +1,129 @@ + + + + + diff --git a/components/TypingIndicator.uvue b/components/TypingIndicator.uvue new file mode 100644 index 0000000..d95ce52 --- /dev/null +++ b/components/TypingIndicator.uvue @@ -0,0 +1,53 @@ + + + + + diff --git a/components/WorkflowChip.uvue b/components/WorkflowChip.uvue new file mode 100644 index 0000000..8b00877 --- /dev/null +++ b/components/WorkflowChip.uvue @@ -0,0 +1,65 @@ + + + + + diff --git a/components/WorkflowSelector.uvue b/components/WorkflowSelector.uvue new file mode 100644 index 0000000..9259d9e --- /dev/null +++ b/components/WorkflowSelector.uvue @@ -0,0 +1,67 @@ + + + + + diff --git a/components/WorkspaceItem.uvue b/components/WorkspaceItem.uvue new file mode 100644 index 0000000..8bf472c --- /dev/null +++ b/components/WorkspaceItem.uvue @@ -0,0 +1,132 @@ + + + + + diff --git a/components/WorkspaceView.uvue b/components/WorkspaceView.uvue new file mode 100644 index 0000000..a5bb3a9 --- /dev/null +++ b/components/WorkspaceView.uvue @@ -0,0 +1,81 @@ + + + + + diff --git a/composables/useConversation.uts b/composables/useConversation.uts new file mode 100644 index 0000000..5ff4fa9 --- /dev/null +++ b/composables/useConversation.uts @@ -0,0 +1,259 @@ +/** + * useConversation — Active conversation management + * + * The core business logic composable: + * - Manages the currently active conversation + * - Handles sending messages and receiving mock AI responses + * - Auto-generates workspace items for substantial responses + * - Coordinates between useConversations, useWorkflow, and useWorkspace + */ + +import type { Message, WorkflowId, Conversation, WorkspaceItem } from '@/types/index' +import { STORAGE_KEYS, generateId, MOCK_AI_DELAY_MS } from '@/constants/index' +import { useStorage } from './useStorage' + +// Mock AI response templates per workflow +const AI_RESPONSES: Record = { + general: [ + '这是一个很好的问题!让我来为你详细解答。\n\n首先,我们需要理解核心概念。人工智能正在快速发展,已经渗透到我们生活的方方面面。\n\n**关键要点:**\n- 保持好奇心,不断学习新知识\n- 实践是检验真理的唯一标准\n- 多角度思考问题能带来更好的解决方案\n\n希望这些对你有所帮助!还有其他问题吗?', + '感谢你的提问!关于这个问题,我有以下几点建议:\n\n1. **明确目标**:先确定你想要达到的具体结果\n2. **制定计划**:把大目标拆分成小步骤\n3. **持续迭代**:在执行过程中不断优化调整\n\n如果你能提供更多具体信息,我可以给出更具针对性的建议。', + '让我从几个角度来分析这个问题:\n\n从技术层面看,这涉及到系统的整体架构设计。从用户体验角度来说,简洁直观的交互至关重要。\n\n综合考虑,我建议采用渐进式的方法,先验证核心功能,再逐步扩展。你觉得这个思路如何?', + ], + code: [ + '好的,让我来帮你解决这个编程问题。\n\n```\nfunction solve(arr: number[]): number {\n const n = arr.length\n let result = 0\n for (let i = 0; i < n; i++) {\n result += arr[i]\n }\n return result\n}\n```\n\n这段代码的时间复杂度是 O(n),空间复杂度是 O(1)。\n\n**注意事项:**\n- 记得处理边界情况(空数组)\n- 可以考虑使用 reduce 方法简化代码\n- 如果需要处理大数,注意整数溢出问题\n\n还有什么需要我帮忙的吗?', + '这是一个经典的设计模式问题。我推荐使用以下架构:\n\n```\nclass EventBus {\n private handlers: Map\n \n on(event: string, handler: Function) {\n // 注册事件处理器\n }\n \n emit(event: string, data: any) {\n // 触发事件\n }\n}\n```\n\n这种发布-订阅模式可以有效解耦模块间的依赖,让代码更具可维护性。', + '让我看看你的代码逻辑...\n\n问题在于异步操作的处理。你需要使用 `await` 来等待 Promise 完成:\n\n```\nasync function fetchData(url: string) {\n try {\n const response = await fetch(url)\n const data = await response.json()\n return data\n } catch (error) {\n console.error(\'Failed to fetch:\', error)\n throw error\n }\n}\n```\n\n另外建议添加错误处理和重试机制,提高代码的健壮性。', + ], + document: [ + '好的,我来帮你撰写这篇文档。\n\n---\n\n**引言**\n\n在当今快速发展的数字化时代,企业面临着前所未有的机遇与挑战。如何有效利用技术手段提升业务效率,已成为每个管理者必须思考的问题。\n\n**核心观点**\n\n- 数字化转型不仅仅是技术升级,更是思维方式的转变\n- 数据驱动决策能够显著提升业务精准度\n- 用户体验应始终放在产品设计的首位\n\n**总结**\n\n通过系统化的方法推进数字化转型,企业能够在竞争激烈的市场中保持领先地位。\n\n---\n\n以上是初稿,你觉得需要调整什么方向吗?', + '帮你润色了一下这段文字:\n\n**原文**:这个产品很好用,功能很多,界面也好看。\n\n**润色后**:这款产品表现优秀,不仅功能丰富全面,界面设计也简洁美观,为用户带来了出色的使用体验。\n\n**修改要点:**\n- 用词更加正式专业\n- 句式更加丰富多变\n- 增强了文字的感染力\n\n需要继续优化其他内容吗?', + ], + data: [ + '根据你提供的数据,我进行了详细分析:\n\n📊 **数据概览**\n\n| 指标 | 当前值 | 环比变化 |\n|------|--------|----------|\n| 用户数 | 12,580 | +8.3% |\n| 活跃率 | 67.2% | +2.1% |\n| 转化率 | 3.8% | -0.5% |\n\n🔍 **关键洞察**\n\n1. 用户增长保持良好势头,增长率超过行业平均水平\n2. 活跃率稳步提升,说明产品粘性增强\n3. 转化率略有下降,需要关注转化漏斗的优化\n\n💡 **建议措施**\n\n- 针对转化率问题,优化注册流程\n- 分析高活跃用户的行为特征并复制推广\n- 关注用户留存曲线的变化趋势\n\n需要我深入分析某个具体指标吗?', + '让我从数据角度来分析这个趋势:\n\n**相关性分析结果:**\n- 变量A与变量B的相关系数:0.87(强正相关)\n- 变量C与结果的相关系数:-0.42(中等负相关)\n\n这意味着当A增加时,B也倾向于增加。而C的增加反而会抑制结果的提升。\n\n**统计显著性**:p值 < 0.01,结果具有统计意义。\n\n建议重点关注A和B的关系,这可能是一个关键的驱动因素。', + ], + creative: [ + '好的,让我来一场头脑风暴!🎯\n\n**方案一:社交裂变**\n利用用户的社交网络,通过分享激励机制实现病毒式传播。核心是设计一个让用户"忍不住想分享"的体验。\n\n**方案二:内容IP化**\n打造独特的品牌IP形象,通过故事化内容与用户建立情感连接。IP可以出现在产品各个触点。\n\n**方案三:场景化营销**\n深入用户真实使用场景,在不同场景下提供定制化的解决方案。让产品"恰好"出现在用户需要的时候。\n\n💡 我个人最看好方案二的长期价值,但方案一能在短期内带来快速增长。你觉得哪个方向更符合你的目标?', + '让我们跳出常规思维,重新构想这个问题的解决方案!\n\n想象一下,如果完全没有技术和资源的限制,理想的解决方案会是什么样?\n\n从这个"理想态"往回推导:\n1. 理想体验的核心要素是什么?\n2. 哪些要素可以通过现有技术实现?\n3. 哪些需要创新的替代方案?\n\n这种"反向思维"往往能激发出意想不到的创意。比如Airbnb就是通过"让陌生人住进家里"这个看似疯狂的创意颠覆了酒店行业。\n\n你有什么初步的想法吗?我们一起碰撞出更多灵感!', + ], +} + +/** + * @param conversationsApi — functions from useConversations(): getConversation, saveConversation, createConversation + * @param getActiveWorkflowId — getter function that returns the current active workflow ID from useWorkflow() + * @param addWorkspaceItemFn — function from useWorkspace() to add workspace items. + * All are passed in as dependencies so the same state is shared with the page. + */ +export function useConversation( + conversationsApi: { + getConversation: (id: string) => Conversation | undefined + saveConversation: (conv: Conversation) => void + createConversation: (workflowId: WorkflowId, title?: string) => Conversation + }, + getActiveWorkflowId: () => WorkflowId, + addWorkspaceItemFn?: (item: WorkspaceItem) => void +) { + const { loadValue, saveValue } = useStorage() + const { getConversation, saveConversation, createConversation } = conversationsApi + + // Fallback no-op if not provided + const addWorkspaceItem = addWorkspaceItemFn ?? (() => {}) + + // Active conversation ID + const activeConversationId = ref( + loadValue(STORAGE_KEYS.ACTIVE_CONVERSATION_ID, null) + ) + + // Whether the AI is currently "typing" + const isLoading = ref(false) + + /** + * Messages of the currently active conversation (computed). + */ + const messages = computed(() => { + if (!activeConversationId.value) return [] + const conv = getConversation(activeConversationId.value) + return conv ? conv.messages : [] + }) + + /** + * Set the active conversation ID and persist. + */ + function setActiveConversationId(id: string | null): void { + activeConversationId.value = id + if (id) { + saveValue(STORAGE_KEYS.ACTIVE_CONVERSATION_ID, id) + } else { + saveValue(STORAGE_KEYS.ACTIVE_CONVERSATION_ID, null) + } + } + + /** + * Start a new conversation with the given workflow. + */ + function startNewConversation(workflowId: WorkflowId): void { + const conv = createConversation(workflowId) + setActiveConversationId(conv.id) + isLoading.value = false + } + + /** + * Load an existing conversation by ID. + */ + function loadConversation(id: string): void { + const conv = getConversation(id) + if (conv) { + setActiveConversationId(id) + isLoading.value = false + } + } + + /** + * Send a message from the user. + * Adds the user message, then triggers a mock AI response after a delay. + */ + function sendMessage(content: string): void { + if (!content.trim() || isLoading.value) return + + // Ensure we have an active conversation + if (!activeConversationId.value) { + startNewConversation(getActiveWorkflowId()) + } + + const convId: string = activeConversationId.value ?? '' + if (!convId) return + + const conv = getConversation(convId) + if (!conv) { + startNewConversation(getActiveWorkflowId()) + // Retry after creating + sendMessage(content) + return + } + + // Add user message + const userMessage: Message = { + id: generateId(), + role: 'user', + content: content.trim(), + timestamp: Date.now(), + status: 'sent', + } + + conv.messages.push(userMessage) + conv.updatedAt = Date.now() + + // Auto-title: use first user message as conversation title + if (conv.messages.filter((m) => m.role === 'user').length === 1) { + let title = content.trim() + if (title.length > 20) { + title = title.substring(0, 20) + '...' + } + conv.title = title + } + + saveConversation(conv) + + // Trigger mock AI response + isLoading.value = true + setTimeout(() => { + simulateAiResponse(conv) + }, MOCK_AI_DELAY_MS) + } + + /** + * Retry sending a message (for error recovery). + */ + function retryMessage(messageId: string): void { + const conv = getConversation(activeConversationId.value ?? '') + if (!conv) return + + const msg = conv.messages.find((m) => m.id === messageId) + if (msg) { + msg.status = 'sent' + saveConversation(conv) + } + } + + /** + * Simulate an AI response. + * In production, replace this with a real API call. + */ + function simulateAiResponse(conv: Conversation): void { + const responses = AI_RESPONSES[conv.workflowId] ?? AI_RESPONSES['general'] + const responseContent = responses[Math.floor(Math.random() * responses.length)] + + const aiMessage: Message = { + id: generateId(), + role: 'assistant', + content: responseContent, + timestamp: Date.now(), + status: 'sent', + } + + // Get fresh reference in case conversations array changed + const freshConv = getConversation(conv.id) + if (!freshConv) { + isLoading.value = false + return + } + + freshConv.messages.push(aiMessage) + freshConv.updatedAt = Date.now() + saveConversation(freshConv) + + // Auto-generate workspace item for substantial responses + if (conv.workflowId !== 'general') { + generateWorkspaceItem(freshConv, aiMessage) + } + + isLoading.value = false + } + + /** + * Generate a workspace item from an AI response. + */ + function generateWorkspaceItem(conv: Conversation, message: Message): void { + const typeMap: Record = { + code: 'code_snippet', + document: 'document', + data: 'analysis_report', + creative: 'creative_output', + } + + const titles: Record = { + code: '代码片段', + document: '文档草稿', + data: '分析报告', + creative: '创意方案', + } + + const itemType = (typeMap[conv.workflowId] ?? 'document') as import('@/types/index').WorkspaceItemType + const autoTitle = titles[conv.workflowId] ?? '工作结果' + + // Only create if response is long enough to be "substantial" + if (message.content.length < 50) return + + addWorkspaceItem({ + id: generateId(), + conversationId: conv.id, + workflowId: conv.workflowId, + type: itemType, + title: `${autoTitle} - ${conv.title}`, + content: message.content, + summary: message.content.substring(0, 120) + '...', + createdAt: Date.now(), + }) + } + + return { + activeConversationId, + messages, + isLoading, + sendMessage, + retryMessage, + loadConversation, + startNewConversation, + setActiveConversationId, + } +} diff --git a/composables/useConversations.uts b/composables/useConversations.uts new file mode 100644 index 0000000..86907b3 --- /dev/null +++ b/composables/useConversations.uts @@ -0,0 +1,93 @@ +/** + * useConversations — Conversation CRUD and history list management + * + * Manages the full array of all conversations. Persisted to storage. + * Provides create, delete, update, and query operations. + */ + +import type { Conversation, WorkflowId } from '@/types/index' +import { STORAGE_KEYS, generateId } from '@/constants/index' +import { useStorage } from './useStorage' + +export function useConversations() { + const { loadValue, saveValue } = useStorage() + + // All conversations loaded from persistence + const conversations = ref( + loadValue(STORAGE_KEYS.CONVERSATIONS, []) + ) + + /** + * Persist the current conversations array to storage. + */ + function persist(): void { + saveValue(STORAGE_KEYS.CONVERSATIONS, conversations.value) + } + + /** + * Create a new empty conversation with the given workflow. + */ + function createConversation(workflowId: WorkflowId, title?: string): Conversation { + const now = Date.now() + const conversation: Conversation = { + id: generateId(), + title: title ?? '新对话', + workflowId: workflowId, + messages: [], + createdAt: now, + updatedAt: now, + } + conversations.value.unshift(conversation) + persist() + return conversation + } + + /** + * Delete a conversation by ID. + */ + function deleteConversation(id: string): void { + conversations.value = conversations.value.filter((c) => c.id !== id) + persist() + } + + /** + * Update a conversation's title. + */ + function updateConversationTitle(id: string, title: string): void { + const conv = conversations.value.find((c) => c.id === id) + if (conv) { + conv.title = title + conv.updatedAt = Date.now() + persist() + } + } + + /** + * Get a conversation by ID. + */ + function getConversation(id: string): Conversation | undefined { + return conversations.value.find((c) => c.id === id) + } + + /** + * Update a conversation's messages and timestamp. + * Used after adding/removing messages. + */ + function saveConversation(conversation: Conversation): void { + const index = conversations.value.findIndex((c) => c.id === conversation.id) + if (index !== -1) { + conversations.value[index] = conversation + persist() + } + } + + return { + conversations, + createConversation, + deleteConversation, + updateConversationTitle, + getConversation, + saveConversation, + persist, + } +} diff --git a/composables/useDrawer.uts b/composables/useDrawer.uts new file mode 100644 index 0000000..40bdb3c --- /dev/null +++ b/composables/useDrawer.uts @@ -0,0 +1,29 @@ +/** + * useDrawer — Drawer open/close state management + * + * Controls the left-slide drawer that contains history and workspace views. + * Simple boolean toggle — no persistence needed. + */ + +export function useDrawer() { + const isDrawerOpen = ref(false) + + function openDrawer(): void { + isDrawerOpen.value = true + } + + function closeDrawer(): void { + isDrawerOpen.value = false + } + + function toggleDrawer(): void { + isDrawerOpen.value = !isDrawerOpen.value + } + + return { + isDrawerOpen, + openDrawer, + closeDrawer, + toggleDrawer, + } +} diff --git a/composables/useStorage.uts b/composables/useStorage.uts new file mode 100644 index 0000000..7fe96dc --- /dev/null +++ b/composables/useStorage.uts @@ -0,0 +1,53 @@ +/** + * useStorage — Typed persistence wrapper around uni.getStorageSync/setStorageSync + */ + +export function useStorage() { + + /** + * Load a value from storage with a fallback default. + * Returns fallback if key doesn't exist or JSON parsing fails. + */ + function loadValue(key: string, fallback: T): T { + try { + const raw = uni.getStorageSync(key) + if (raw === '' || raw === undefined || raw === null) { + return fallback + } + const parsed = JSON.parse(raw) as T + return parsed !== null ? parsed : fallback + } catch (_e) { + console.warn(`[useStorage] Failed to load key "${key}", using fallback`) + return fallback + } + } + + /** + * Save a value to storage as JSON. + */ + function saveValue(key: string, value: T): void { + try { + const raw = JSON.stringify(value) + uni.setStorageSync(key, raw) + } catch (e) { + console.error(`[useStorage] Failed to save key "${key}":`, e) + } + } + + /** + * Remove a key from storage. + */ + function removeValue(key: string): void { + try { + uni.removeStorageSync(key) + } catch (e) { + console.error(`[useStorage] Failed to remove key "${key}":`, e) + } + } + + return { + loadValue, + saveValue, + removeValue, + } +} diff --git a/composables/useWorkflow.uts b/composables/useWorkflow.uts new file mode 100644 index 0000000..4e0e7c8 --- /dev/null +++ b/composables/useWorkflow.uts @@ -0,0 +1,43 @@ +/** + * useWorkflow — Current workflow selection state + * + * Manages which workflow is active (general, code, document, data, creative). + * The selection is persisted so it survives app restarts. + */ + +import type { WorkflowId, WorkflowDef } from '@/types/index' +import { WORKFLOWS, STORAGE_KEYS } from '@/constants/index' +import { useStorage } from './useStorage' + +export function useWorkflow() { + const { loadValue, saveValue } = useStorage() + + // Initialize from storage, fallback to 'general' + const activeWorkflowId = ref( + loadValue(STORAGE_KEYS.ACTIVE_WORKFLOW_ID, 'general') + ) + + // All available workflows (static) + const workflows: WorkflowDef[] = WORKFLOWS + + // Computed: the currently active workflow definition + const activeWorkflow = computed(() => { + const found = workflows.find((w) => w.id === activeWorkflowId.value) + return found ?? workflows[0] + }) + + /** + * Select a workflow and persist the choice. + */ + function selectWorkflow(id: WorkflowId): void { + activeWorkflowId.value = id + saveValue(STORAGE_KEYS.ACTIVE_WORKFLOW_ID, id) + } + + return { + activeWorkflowId, + workflows, + activeWorkflow, + selectWorkflow, + } +} diff --git a/composables/useWorkspace.uts b/composables/useWorkspace.uts new file mode 100644 index 0000000..643b3ff --- /dev/null +++ b/composables/useWorkspace.uts @@ -0,0 +1,69 @@ +/** + * useWorkspace — Workspace items management + * + * Manages workspace items generated from AI workflow responses. + * Items are persisted and filterable by workflow type. + */ + +import type { WorkspaceItem, WorkflowId } from '@/types/index' +import { STORAGE_KEYS } from '@/constants/index' +import { useStorage } from './useStorage' + +export function useWorkspace() { + const { loadValue, saveValue } = useStorage() + + const workspaceItems = ref( + loadValue(STORAGE_KEYS.WORKSPACE, []) + ) + + function persist(): void { + saveValue(STORAGE_KEYS.WORKSPACE, workspaceItems.value) + } + + /** + * Add a new workspace item. + */ + function addWorkspaceItem(item: WorkspaceItem): void { + workspaceItems.value.unshift(item) + persist() + } + + /** + * Remove a workspace item by ID. + */ + function removeWorkspaceItem(id: string): void { + workspaceItems.value = workspaceItems.value.filter((item) => item.id !== id) + persist() + } + + /** + * Get workspace items filtered by workflow type. + */ + function getItemsByWorkflow(workflowId: WorkflowId): WorkspaceItem[] { + return workspaceItems.value.filter((item) => item.workflowId === workflowId) + } + + /** + * Get workspace item by ID. + */ + function getItemById(id: string): WorkspaceItem | undefined { + return workspaceItems.value.find((item) => item.id === id) + } + + /** + * Clear all workspace items. + */ + function clearAll(): void { + workspaceItems.value = [] + persist() + } + + return { + workspaceItems, + addWorkspaceItem, + removeWorkspaceItem, + getItemsByWorkflow, + getItemById, + clearAll, + } +} diff --git a/constants/index.uts b/constants/index.uts new file mode 100644 index 0000000..2ed67db --- /dev/null +++ b/constants/index.uts @@ -0,0 +1,65 @@ +/** + * AI Chat Assistant — Constants + */ + +import type { WorkflowDef } from '@/types/index' + +// Workflow definitions +export const WORKFLOWS: WorkflowDef[] = [ + { + id: 'general', + name: '通用对话', + icon: '💬', + description: '自由提问,获取AI智能回答', + color: '#6c5ce7', + }, + { + id: 'code', + name: '代码助手', + icon: '💻', + description: '编程问题解答、代码生成与调试', + color: '#00b894', + }, + { + id: 'document', + name: '文档写作', + icon: '📝', + description: '文章撰写、润色与总结', + color: '#fdcb6e', + }, + { + id: 'data', + name: '数据分析', + icon: '📊', + description: '数据处理、图表解读与洞察分析', + color: '#e17055', + }, + { + id: 'creative', + name: '创意生成', + icon: '✨', + description: '头脑风暴、创意点子与方案策划', + color: '#a29bfe', + }, +] + +// Storage keys +export const STORAGE_KEYS = { + CONVERSATIONS: 'asst_conversations', + WORKSPACE: 'asst_workspace', + ACTIVE_CONVERSATION_ID: 'asst_active_conv_id', + ACTIVE_WORKFLOW_ID: 'asst_workflow_id', +} as const + +// App settings +export const MOCK_AI_DELAY_MS = 1500 +export const MAX_INPUT_LENGTH = 2000 +export const APP_NAME = 'AI助手' +export const APP_SUBTITLE = '智能工作助手' + +// Generate a simple unique ID +export function generateId(): string { + const timestamp = Date.now().toString(36) + const random = Math.floor(Math.random() * 100000).toString(36) + return `${timestamp}${random}` +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..8793fef --- /dev/null +++ b/index.html @@ -0,0 +1,20 @@ + + + + + + + + + + +
+ + + \ No newline at end of file diff --git a/main.uts b/main.uts new file mode 100644 index 0000000..8bdcc86 --- /dev/null +++ b/main.uts @@ -0,0 +1,9 @@ +import App from './App.uvue' + +import { createSSRApp } from 'vue' +export function createApp() { + const app = createSSRApp(App) + return { + app + } +} \ No newline at end of file diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..4f6d427 --- /dev/null +++ b/manifest.json @@ -0,0 +1,43 @@ +{ + "name" : "AI助手", + "appid" : "", + "description" : "智能AI聊天助手 - 多工作流对话、工作空间、历史记录", + "versionName" : "1.0.0", + "versionCode" : "100", + "uni-app-x" : {}, + /* 快应用特有相关 */ + "quickapp" : {}, + /* 小程序特有相关 */ + "mp-weixin" : { + "appid" : "", + "setting" : { + "urlCheck" : false + }, + "usingComponents" : true + }, + "mp-alipay" : { + "usingComponents" : true + }, + "mp-baidu" : { + "usingComponents" : true + }, + "mp-toutiao" : { + "usingComponents" : true + }, + "uniStatistics" : { + "enable" : false + }, + "vueVersion" : "3", + "app" : { + "distribute" : { + "icons" : { + "android" : { + "hdpi" : "", + "xhdpi" : "", + "xxhdpi" : "", + "xxxhdpi" : "" + } + } + } + } +} diff --git a/pages.json b/pages.json new file mode 100644 index 0000000..17e0f31 --- /dev/null +++ b/pages.json @@ -0,0 +1,19 @@ +{ + "pages": [ + { + "path": "pages/index/index", + "style": { + "navigationStyle": "custom", + "navigationBarTitleText": "AI助手", + "softinputMode": "adjustResize" + } + } + ], + "globalStyle": { + "navigationBarTextStyle": "white", + "navigationBarTitleText": "AI助手", + "navigationBarBackgroundColor": "#0f0f1a", + "backgroundColor": "#0f0f1a" + }, + "uniIdRouter": {} +} diff --git a/pages/index/index.uvue b/pages/index/index.uvue new file mode 100644 index 0000000..1d2e9ea --- /dev/null +++ b/pages/index/index.uvue @@ -0,0 +1,425 @@ + + + + + diff --git a/platformConfig.json b/platformConfig.json new file mode 100644 index 0000000..896f669 --- /dev/null +++ b/platformConfig.json @@ -0,0 +1,7 @@ + +// 参考链接 https://doc.dcloud.net.cn/uni-app-x/tutorial/ls-plugin.html#setting +{ + "targets": [ + "APP-ANDROID" + ] +} \ No newline at end of file diff --git a/static/logo.png b/static/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..b5771e209bb677e2ebd5ff766ad5ee11790f305a GIT binary patch literal 4023 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 SYs}WS&%*SCNx89m_