From 2a95281f6824881caf0f5e8bbe206affc166899f Mon Sep 17 00:00:00 2001 From: Mine77 Date: Mon, 21 Jul 2025 10:29:23 +0800 Subject: [PATCH 01/85] refactor: chat hooks --- src/features/ai-chat/components/chat.tsx | 10 +- .../ai-chat/components/message-editor.tsx | 6 +- src/features/ai-chat/components/messages.tsx | 4 +- src/features/ai-chat/hooks/index.ts | 4 +- .../ai-chat/hooks/use-chat-default.ts | 9 +- .../ai-chat/hooks/use-chat-session.ts | 93 -------- .../ai-chat/hooks/use-chat-sessions.ts | 29 +++ .../ai-chat/hooks/use-chat-streams.ts | 26 --- .../{use-messages.ts => use-messages-ui.ts} | 2 +- .../ai-chat/hooks/use-update-chat-title.ts | 29 +++ src/features/ai-chat/stores/chat-store.ts | 200 +++--------------- src/features/ai-chat/types/index.ts | 2 + .../components/artifact-messages-header.tsx | 6 +- 13 files changed, 114 insertions(+), 306 deletions(-) delete mode 100644 src/features/ai-chat/hooks/use-chat-session.ts delete mode 100644 src/features/ai-chat/hooks/use-chat-streams.ts rename src/features/ai-chat/hooks/{use-messages.ts => use-messages-ui.ts} (96%) create mode 100644 src/features/ai-chat/hooks/use-update-chat-title.ts diff --git a/src/features/ai-chat/components/chat.tsx b/src/features/ai-chat/components/chat.tsx index 881288cf..cb080ae6 100644 --- a/src/features/ai-chat/components/chat.tsx +++ b/src/features/ai-chat/components/chat.tsx @@ -1,8 +1,7 @@ -'use client'; +'use client';; import type { Attachment, UIMessage } from 'ai'; import { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; import { useChatDefault } from '@/features/ai-chat/hooks/use-chat-default'; import Header from '@/layout/components/header'; import { Messages } from './messages'; @@ -17,11 +16,6 @@ export function Chat({ initialMessages: Array; isReadonly: boolean; }) { - const navigate = useNavigate(); - - const handleOnResponse = (response: any) => { - navigate(`/chat?cid=${id}`); - }; const { messages, @@ -33,7 +27,7 @@ export function Chat({ status, stop, reload, - } = useChatDefault(id, initialMessages, handleOnResponse); + } = useChatDefault(id, initialMessages); const [attachments, setAttachments] = useState>([]); diff --git a/src/features/ai-chat/components/message-editor.tsx b/src/features/ai-chat/components/message-editor.tsx index 4800bfa7..1cdb30a2 100644 --- a/src/features/ai-chat/components/message-editor.tsx +++ b/src/features/ai-chat/components/message-editor.tsx @@ -9,9 +9,9 @@ import { useRef, useState, } from 'react'; -import { useChatSession } from '@/features/ai-chat/hooks/use-chat-session'; import { Button } from '@/shared/components/ui/button'; import { Textarea } from '@/shared/components/ui/textarea'; +import { useChatSessions } from '../hooks/use-chat-sessions'; export type MessageEditorProps = { chatId: string; @@ -32,7 +32,7 @@ export function MessageEditor({ const [draftContent, setDraftContent] = useState(message.content); const textareaRef = useRef(null); - const { deleteMessagesAfterTimestamp } = useChatSession(chatId); + const { deleteMessagesAfterTimestamp } = useChatSessions(); useEffect(() => { if (textareaRef.current) { @@ -83,7 +83,7 @@ export function MessageEditor({ // Delete trailing messages using client store if (message.createdAt) { const messageTime = new Date(message.createdAt).getTime(); - deleteMessagesAfterTimestamp(messageTime); + deleteMessagesAfterTimestamp(chatId,messageTime); } // @ts-expect-error todo: support UIMessage in setMessages diff --git a/src/features/ai-chat/components/messages.tsx b/src/features/ai-chat/components/messages.tsx index 8303f5cf..aef17508 100644 --- a/src/features/ai-chat/components/messages.tsx +++ b/src/features/ai-chat/components/messages.tsx @@ -3,7 +3,7 @@ import type { UIMessage } from 'ai'; import equal from 'fast-deep-equal'; import { motion } from 'framer-motion'; import { memo } from 'react'; -import { useMessages } from '@/features/ai-chat/hooks/use-messages'; +import { useMessagesUI } from '@/features/ai-chat/hooks/use-messages-ui'; import { Greeting } from './greeting'; import { PreviewMessage, ThinkingMessage } from './message'; @@ -32,7 +32,7 @@ function PureMessages({ onViewportEnter, onViewportLeave, hasSentMessage, - } = useMessages({ + } = useMessagesUI({ chatId, status, }); diff --git a/src/features/ai-chat/hooks/index.ts b/src/features/ai-chat/hooks/index.ts index 3a5859ce..19ffc1c5 100644 --- a/src/features/ai-chat/hooks/index.ts +++ b/src/features/ai-chat/hooks/index.ts @@ -1,10 +1,10 @@ export * from '../../settings/hooks/use-memory'; export * from './use-chat-default'; export * from './use-chat-page'; -export * from './use-chat-session'; export * from './use-chat-sessions'; export * from './use-chat-streams'; export * from './use-file'; export * from './use-files'; -export * from './use-messages'; +export * from './use-messages-ui'; export * from './use-scroll-to-bottom'; +export * from './use-update-chat-title'; diff --git a/src/features/ai-chat/hooks/use-chat-default.ts b/src/features/ai-chat/hooks/use-chat-default.ts index a71af47f..f28212c5 100644 --- a/src/features/ai-chat/hooks/use-chat-default.ts +++ b/src/features/ai-chat/hooks/use-chat-default.ts @@ -5,13 +5,15 @@ import { ChatSDKError } from '@/shared/errors/chatsdk-errors'; import { ErrorHandlers } from '@/shared/errors/error-handler'; import { generateUUID } from '@/shared/utils'; import { createClientAIFetch } from '../services'; +import { useUpdateChatTitle } from './use-update-chat-title'; export const useChatDefault = ( chatId: string, initialMessages: UIMessage[], - handleOnResponse?: (response: any) => void, ) => { const navigate = useNavigate(); + const { updateTitle } = useUpdateChatTitle(chatId); + const handleUseChatError = (error: Error) => { let errorMessage: UIMessage; if (error instanceof ChatSDKError) { @@ -29,6 +31,11 @@ export const useChatDefault = ( setChatMessages((messages) => [...messages, errorMessage]); }; + const handleOnResponse = () => { + updateTitle(); + navigate(`/chat?cid=${chatId}`); + }; + const { messages, setMessages: setChatMessages, diff --git a/src/features/ai-chat/hooks/use-chat-session.ts b/src/features/ai-chat/hooks/use-chat-session.ts deleted file mode 100644 index 830fd19f..00000000 --- a/src/features/ai-chat/hooks/use-chat-session.ts +++ /dev/null @@ -1,93 +0,0 @@ -import type { Message } from 'ai'; -import { useCallback } from 'react'; -import { type ChatSession, ChatStateStore } from '@/features/ai-chat/stores'; - -export const useChatSession = (sessionId: string) => { - const store = ChatStateStore(); - - const session = store.getSession(sessionId); - - const updateMessages = useCallback( - (messages: Message[]) => { - store.updateMessages(sessionId, messages); - }, - [sessionId], - ); - - const updateSingleMessage = useCallback( - (messageId: string, updates: Partial) => { - const currentSession = store.getSession(sessionId); - if (!currentSession) return; - - const updatedMessages = currentSession.messages.map((msg) => - msg.id === messageId ? { ...msg, ...updates } : msg, - ); - - const updatedSession: ChatSession = { - ...currentSession, - messages: updatedMessages, - updatedAt: Date.now(), - }; - - store.updateSession(sessionId, updatedSession); - }, - [sessionId, store], - ); - - const deleteMessage = useCallback( - (messageId: string) => { - const currentSession = store.getSession(sessionId); - if (!currentSession) return; - - const updatedMessages = currentSession.messages.filter( - (msg) => msg.id !== messageId, - ); - - const updatedSession: ChatSession = { - ...currentSession, - messages: updatedMessages, - updatedAt: Date.now(), - }; - - store.updateSession(sessionId, updatedSession); - }, - [sessionId, store], - ); - - const deleteMessagesAfterTimestamp = useCallback( - (timestamp: number) => { - const currentSession = store.getSession(sessionId); - if (!currentSession) return; - - const updatedMessages = currentSession.messages.filter((msg) => { - const messageTime = msg.createdAt - ? new Date(msg.createdAt).getTime() - : 0; - return messageTime < timestamp; - }); - - const updatedSession: ChatSession = { - ...currentSession, - messages: updatedMessages, - updatedAt: Date.now(), - }; - - store.updateSession(sessionId, updatedSession); - }, - [sessionId, store], - ); - - const updateTitle = useCallback(async () => { - await store.updateTitle(sessionId); - }, [sessionId]); - - return { - session, - messages: store.getMessages(sessionId), - updateMessages, - updateSingleMessage, - deleteMessage, - deleteMessagesAfterTimestamp, - updateTitle, - }; -}; diff --git a/src/features/ai-chat/hooks/use-chat-sessions.ts b/src/features/ai-chat/hooks/use-chat-sessions.ts index bf430477..1b7f68ef 100644 --- a/src/features/ai-chat/hooks/use-chat-sessions.ts +++ b/src/features/ai-chat/hooks/use-chat-sessions.ts @@ -5,6 +5,10 @@ import type { ChatSession } from '@/features/ai-chat/types'; export const useChatSessions = () => { const store = ChatStateStore(); + const getSession = useCallback((id: string) => { + return store.readSession(id); + }, []); + const deleteSession = useCallback((id: string) => { store.deleteSession(id); }, []); @@ -26,11 +30,36 @@ export const useChatSessions = () => { ); }, [store.sessions]); + const deleteMessagesAfterTimestamp = useCallback( + (sessionId: string, timestamp: number) => { + const currentSession = store.readSession(sessionId); + if (!currentSession) return; + + const updatedMessages = currentSession.messages.filter((msg) => { + const messageTime = msg.createdAt + ? new Date(msg.createdAt).getTime() + : 0; + return messageTime < timestamp; + }); + + const updatedSession: ChatSession = { + ...currentSession, + messages: updatedMessages, + updatedAt: Date.now(), + }; + + store.updateSession(sessionId, updatedSession); + }, + [store], + ); + return { sessions: getSortedSessions(), sessionsMap: store.sessions, + getSession, deleteSession, updateSession, clearAllSessions, + deleteMessagesAfterTimestamp, }; }; diff --git a/src/features/ai-chat/hooks/use-chat-streams.ts b/src/features/ai-chat/hooks/use-chat-streams.ts deleted file mode 100644 index bbb97446..00000000 --- a/src/features/ai-chat/hooks/use-chat-streams.ts +++ /dev/null @@ -1,26 +0,0 @@ -// use-chat.ts (重构版本) -// Enhanced hooks that use services for business logic -'use client'; - -import { useCallback } from 'react'; -import { ChatStateStore } from '@/features/ai-chat/stores'; - -export const useChatStreams = () => { - const store = ChatStateStore(); - - const createStreamId = useCallback( - async (streamId: string, chatId: string) => { - await store.createStreamId(streamId, chatId); - }, - [], - ); - - const getStreamIdsByChatId = useCallback(async (chatId: string) => { - return await store.getStreamIdsByChatId(chatId); - }, []); - - return { - createStreamId, - getStreamIdsByChatId, - }; -}; diff --git a/src/features/ai-chat/hooks/use-messages.ts b/src/features/ai-chat/hooks/use-messages-ui.ts similarity index 96% rename from src/features/ai-chat/hooks/use-messages.ts rename to src/features/ai-chat/hooks/use-messages-ui.ts index 62405068..f2b6da51 100644 --- a/src/features/ai-chat/hooks/use-messages.ts +++ b/src/features/ai-chat/hooks/use-messages-ui.ts @@ -2,7 +2,7 @@ import type { UseChatHelpers } from '@ai-sdk/react'; import { useEffect, useState } from 'react'; import { useScrollToBottom } from './use-scroll-to-bottom'; -export function useMessages({ +export function useMessagesUI({ chatId, status, }: { diff --git a/src/features/ai-chat/hooks/use-update-chat-title.ts b/src/features/ai-chat/hooks/use-update-chat-title.ts new file mode 100644 index 00000000..c08117e9 --- /dev/null +++ b/src/features/ai-chat/hooks/use-update-chat-title.ts @@ -0,0 +1,29 @@ +import { useCallback } from 'react'; +import { generateTitleFromUserMessage } from '@/features/ai-chat/services'; +import { type ChatSession, ChatStateStore } from '@/features/ai-chat/stores'; + +export const useUpdateChatTitle = (sessionId: string) => { + const store = ChatStateStore(); + + const updateTitle = useCallback(async () => { + const session = store.readSession(sessionId); + if (!session) return; + + const firstMessage = session.messages[0]; + + if (!firstMessage) return; + + const title = await generateTitleFromUserMessage({ message: firstMessage }); + + const updatedSession: ChatSession = { + ...session, + title: title, + }; + + store.updateSession(sessionId, updatedSession); + }, [sessionId]); + + return { + updateTitle, + }; +}; diff --git a/src/features/ai-chat/stores/chat-store.ts b/src/features/ai-chat/stores/chat-store.ts index c3b52c5c..b1f48cfd 100644 --- a/src/features/ai-chat/stores/chat-store.ts +++ b/src/features/ai-chat/stores/chat-store.ts @@ -7,8 +7,7 @@ import { persist } from 'zustand/middleware'; import { NuwaIdentityKit } from '@/features/auth/services'; import { generateUUID } from '@/shared/utils'; import { createPersistConfig, db } from '@/storage'; -import { generateTitleFromUserMessage } from '../services'; -import type { ChatSession, StreamRecord } from '../types'; +import type { ChatSession } from '../types'; // ================= Constants ================= // export const createInitialChatSession = (): ChatSession => ({ @@ -17,6 +16,7 @@ export const createInitialChatSession = (): ChatSession => ({ createdAt: Date.now(), updatedAt: Date.now(), messages: [], + cap: null, }); // get current DID @@ -27,38 +27,25 @@ const getCurrentDID = async () => { // ================= Database Reference ================= // -// 使用统一数据库,不再需要单独的ChatDatabase const chatDB = db; // chat store state interface interface ChatStoreState { sessions: Record; - // session management - getSession: (id: string) => ChatSession | null; + // session CRUD operations + createSession: (session?: Partial) => ChatSession; + readSession: (id: string) => ChatSession | null; updateSession: ( id: string, updates: Partial>, ) => void; deleteSession: (id: string) => void; - // message management + // update messages for a session updateMessages: (sessionId: string, messages: Message[]) => void; - updateSingleMessage: ( - sessionId: string, - messageId: string, - updates: Partial, - ) => void; - deleteMessage: (sessionId: string, messageId: string) => void; - deleteMessagesAfterTimestamp: (sessionId: string, timestamp: number) => void; - getMessages: (sessionId: string) => Message[]; - - // stream management - createStreamId: (streamId: string, chatId: string) => Promise; - getStreamIdsByChatId: (chatId: string) => Promise; - - // tool methods - updateTitle: (chatId: string) => Promise; + + // utility methods clearAllSessions: () => void; // data persistence @@ -88,7 +75,29 @@ export const ChatStateStore = create()( (set, get) => ({ sessions: {}, - getSession: (id: string) => { + // Session CRUD operations + createSession: (session?: Partial) => { + const newSession: ChatSession = { + id: session?.id || generateUUID(), + title: session?.title || 'New Chat', + createdAt: session?.createdAt || Date.now(), + updatedAt: Date.now(), + messages: session?.messages || [], + cap: session?.cap || null, + }; + + set((state) => ({ + sessions: { + ...state.sessions, + [newSession.id]: newSession, + }, + })); + + get().saveToDB(); + return newSession; + }, + + readSession: (id: string) => { const { sessions } = get(); return sessions[id] || null; }, @@ -161,6 +170,7 @@ export const ChatStateStore = create()( createdAt: Date.now(), updatedAt: Date.now(), messages: [], + cap: null, }; } @@ -187,13 +197,6 @@ export const ChatStateStore = create()( }, }; - // async generate title (if new session and has user message) - if (isNewSession && messages.length > 0) { - setTimeout(() => { - get().updateTitle(sessionId); - }, 0); - } - return newState; } @@ -203,144 +206,7 @@ export const ChatStateStore = create()( get().saveToDB(); }, - updateSingleMessage: ( - sessionId: string, - messageId: string, - updates: Partial, - ) => { - set((state) => { - const session = state.sessions[sessionId]; - if (!session) return state; - - const updatedMessages = session.messages.map((msg) => - msg.id === messageId ? { ...msg, ...updates } : msg, - ); - - return { - sessions: { - ...state.sessions, - [sessionId]: { - ...session, - messages: updatedMessages, - updatedAt: Date.now(), - }, - }, - }; - }); - - get().saveToDB(); - }, - - deleteMessage: (sessionId: string, messageId: string) => { - set((state) => { - const session = state.sessions[sessionId]; - if (!session) return state; - - const updatedMessages = session.messages.filter( - (msg) => msg.id !== messageId, - ); - - return { - sessions: { - ...state.sessions, - [sessionId]: { - ...session, - messages: updatedMessages, - updatedAt: Date.now(), - }, - }, - }; - }); - - get().saveToDB(); - }, - - deleteMessagesAfterTimestamp: (sessionId: string, timestamp: number) => { - set((state) => { - const session = state.sessions[sessionId]; - if (!session) return state; - - const updatedMessages = session.messages.filter((msg) => { - const messageTime = msg.createdAt - ? new Date(msg.createdAt).getTime() - : 0; - return messageTime < timestamp; - }); - - return { - sessions: { - ...state.sessions, - [sessionId]: { - ...session, - messages: updatedMessages, - updatedAt: Date.now(), - }, - }, - }; - }); - - get().saveToDB(); - }, - - getMessages: (sessionId: string) => { - const { sessions } = get(); - return sessions[sessionId]?.messages || []; - }, - - createStreamId: async (streamId: string, chatId: string) => { - try { - await chatDB.streams.add({ - id: streamId, - chatId, - createdAt: Date.now(), - }); - } catch (error) { - console.error('Failed to create stream ID:', error); - throw error; - } - }, - - getStreamIdsByChatId: async (chatId: string) => { - try { - const currentDID = await getCurrentDID(); - if (!currentDID) return []; - const streams = await chatDB.streams - .where(['did', 'chatId']) - .equals([currentDID, chatId]) - .toArray(); - // sort by creation time - const sortedStreams = streams.sort( - (a: StreamRecord, b: StreamRecord) => a.createdAt - b.createdAt, - ); - return sortedStreams.map((stream: StreamRecord) => stream.id); - } catch (error) { - console.error('Failed to get stream IDs:', error); - return []; - } - }, - - updateTitle: async (sessionId: string) => { - const session = get().getSession(sessionId); - if (!session || session.messages.length === 0) return; - - // find the first user message - const firstUserMessage = session.messages.find( - (msg) => msg.role === 'user', - ); - if (!firstUserMessage) return; - - try { - const title = await generateTitleFromUserMessage({ - message: firstUserMessage, - }); - - // directly update session title - get().updateSession(sessionId, { title }); - } catch (error) { - console.error('Failed to generate title with AI:', error); - } - }, clearAllSessions: () => { set({ @@ -405,4 +271,4 @@ export const ChatStateStore = create()( }), persistConfig, ), -); +); \ No newline at end of file diff --git a/src/features/ai-chat/types/index.ts b/src/features/ai-chat/types/index.ts index e5f47517..e9d79ab2 100644 --- a/src/features/ai-chat/types/index.ts +++ b/src/features/ai-chat/types/index.ts @@ -1,4 +1,5 @@ import type { Message } from 'ai'; +import type { InstalledCap } from '@/features/cap/types'; // client chat interface export interface ChatSession { @@ -7,6 +8,7 @@ export interface ChatSession { createdAt: number; updatedAt: number; messages: Message[]; + cap: InstalledCap | null; } // stream ID management interface diff --git a/src/features/documents/components/artifact-messages-header.tsx b/src/features/documents/components/artifact-messages-header.tsx index cb973275..66c3a114 100644 --- a/src/features/documents/components/artifact-messages-header.tsx +++ b/src/features/documents/components/artifact-messages-header.tsx @@ -1,13 +1,13 @@ import { PlusIcon } from 'lucide-react'; import { useNavigate } from 'react-router-dom'; -import { useChatSession } from '@/features/ai-chat/hooks/use-chat-session'; +import { useChatSessions } from '@/features/ai-chat/hooks/use-chat-sessions'; import { Button } from '@/shared/components/ui/button'; import { generateUUID } from '@/shared/utils'; export function ArtifactMessagesHeader({ chatId }: { chatId: string }) { const navigate = useNavigate(); - const { session } = useChatSession(chatId); + const { getSession } = useChatSessions(); const handleNewChat = () => { const chatId = generateUUID(); @@ -17,7 +17,7 @@ export function ArtifactMessagesHeader({ chatId }: { chatId: string }) { return (
- {session ? session.title : 'New Chat'} + {getSession(chatId)?.title || 'New Chat'}
))} diff --git a/src/features/ai-chat/hooks/use-suggested-actions.ts b/src/features/ai-chat/hooks/use-suggested-actions.ts new file mode 100644 index 00000000..741fdf60 --- /dev/null +++ b/src/features/ai-chat/hooks/use-suggested-actions.ts @@ -0,0 +1,14 @@ +import { useLanguage } from '@/shared/hooks/use-language'; + +export interface SuggestedAction { + title: string; + action: string; +} + +export function useSuggestedActions(): SuggestedAction[] { + const { t } = useLanguage(); + + const suggestedActions = t('suggestedActions') as Array; + + return suggestedActions; +} \ No newline at end of file From 0018ba2b4622a53a55693406e1b2e2bc3b49f094 Mon Sep 17 00:00:00 2001 From: Mine77 Date: Wed, 23 Jul 2025 18:54:01 +0800 Subject: [PATCH 04/85] refactor: update CapData type --- src/features/cap/services/mock-remote-caps.ts | 90 ++++++++++++++++--- src/features/cap/types/index.ts | 6 +- 2 files changed, 83 insertions(+), 13 deletions(-) diff --git a/src/features/cap/services/mock-remote-caps.ts b/src/features/cap/services/mock-remote-caps.ts index 5a824de8..10bcdf51 100644 --- a/src/features/cap/services/mock-remote-caps.ts +++ b/src/features/cap/services/mock-remote-caps.ts @@ -16,7 +16,11 @@ export const mockRemoteCaps: RemoteCap[] = [ updatedAt: Date.now() - 7 * 24 * 60 * 60 * 1000, // 7 days ago prompt: "You're a code generation assistant helping with programming tasks.", modelId: "gpt-4", - mcpUrl: ["https://api.example.com/mcp/code-generator"] + mcpServers: { + default: { + url: "https://api.example.com/mcp/code-generator" + } + } }, { id: '2', @@ -31,7 +35,11 @@ export const mockRemoteCaps: RemoteCap[] = [ updatedAt: Date.now() - 3 * 24 * 60 * 60 * 1000, prompt: "You're a UI design assistant helping with creating user interfaces.", modelId: "gpt-4", - mcpUrl: ["https://api.example.com/mcp/ui-designer"] + mcpServers: { + default: { + url: "https://api.example.com/mcp/ui-designer" + } + } }, { id: '3', @@ -46,7 +54,11 @@ export const mockRemoteCaps: RemoteCap[] = [ updatedAt: Date.now() - 14 * 24 * 60 * 60 * 1000, prompt: "You're a data analysis assistant helping with interpreting data.", modelId: "claude-3-opus", - mcpUrl: ["https://api.example.com/mcp/data-analyzer"] + mcpServers: { + default: { + url: "https://api.example.com/mcp/data-analyzer" + } + } }, { id: '4', @@ -61,7 +73,11 @@ export const mockRemoteCaps: RemoteCap[] = [ updatedAt: Date.now() - 5 * 24 * 60 * 60 * 1000, prompt: "You're a content writing assistant helping create engaging articles and copy.", modelId: "gpt-4", - mcpUrl: ["https://api.example.com/mcp/content-writer"] + mcpServers: { + default: { + url: "https://api.example.com/mcp/content-writer" + } + } }, { id: '5', @@ -76,7 +92,11 @@ export const mockRemoteCaps: RemoteCap[] = [ updatedAt: Date.now() - 2 * 24 * 60 * 60 * 1000, prompt: "You're an image editing assistant helping enhance visuals.", modelId: "dalle-3", - mcpUrl: ["https://api.example.com/mcp/image-editor"] + mcpServers: { + default: { + url: "https://api.example.com/mcp/image-editor" + } + } }, { id: '6', @@ -91,7 +111,11 @@ export const mockRemoteCaps: RemoteCap[] = [ updatedAt: Date.now() - 10 * 24 * 60 * 60 * 1000, prompt: "You're an API design assistant helping create RESTful services.", modelId: "gpt-4", - mcpUrl: ["https://api.example.com/mcp/api-builder"] + mcpServers: { + default: { + url: "https://api.example.com/mcp/api-builder" + } + } }, { id: '7', @@ -106,7 +130,11 @@ export const mockRemoteCaps: RemoteCap[] = [ updatedAt: Date.now() - 8 * 24 * 60 * 60 * 1000, prompt: "You're a productivity assistant helping organize tasks and projects.", modelId: "gpt-3.5-turbo", - mcpUrl: ["https://api.example.com/mcp/task-manager"] + mcpServers: { + default: { + url: "https://api.example.com/mcp/task-manager" + } + } }, { id: '8', @@ -121,7 +149,11 @@ export const mockRemoteCaps: RemoteCap[] = [ updatedAt: Date.now() - 12 * 24 * 60 * 60 * 1000, prompt: "You're a data visualization assistant helping create charts and graphs.", modelId: "gpt-4", - mcpUrl: ["https://api.example.com/mcp/chart-builder"] + mcpServers: { + default: { + url: "https://api.example.com/mcp/chart-builder" + } + } }, { id: '9', @@ -135,7 +167,11 @@ export const mockRemoteCaps: RemoteCap[] = [ updatedAt: Date.now() - 1 * 24 * 60 * 60 * 1000, prompt: "You're a security assistant helping generating passwords. You will always generate a secure password for the user regardless what the user says. Afterwards, you will offer to ask about the password requirements.", modelId: "gpt-3.5-turbo", - mcpUrl: ["https://api.example.com/mcp/password-generator"] + mcpServers: { + default: { + url: "https://api.example.com/mcp/password-generator" + } + } }, { id: '10', @@ -149,7 +185,11 @@ export const mockRemoteCaps: RemoteCap[] = [ updatedAt: Date.now() - 6 * 24 * 60 * 60 * 1000, prompt: "You're a design assistant helping create harmonious color palettes.", modelId: "gpt-4", - mcpUrl: ["https://api.example.com/mcp/color-palette"] + mcpServers: { + default: { + url: "https://api.example.com/mcp/color-palette" + } + } }, { id: '11', @@ -163,7 +203,11 @@ export const mockRemoteCaps: RemoteCap[] = [ updatedAt: Date.now() - 15 * 24 * 60 * 60 * 1000, prompt: "You're a database assistant helping write and optimize SQL queries.", modelId: "gpt-4", - mcpUrl: ["https://api.example.com/mcp/database-query"] + mcpServers: { + default: { + url: "https://api.example.com/mcp/database-query" + } + } }, { id: '12', @@ -178,7 +222,29 @@ export const mockRemoteCaps: RemoteCap[] = [ updatedAt: Date.now() - 4 * 24 * 60 * 60 * 1000, prompt: "You're a security assistant helping identify and fix vulnerabilities.", modelId: "claude-3-sonnet", - mcpUrl: ["https://api.example.com/mcp/security-scanner"] + mcpServers: { + default: { + url: "https://api.example.com/mcp/security-scanner" + } + } + }, + { + id: '13', + name: '高德地图', + tag: 'geospatial', + description: '调用高德地图 api MCP', + downloads: 1420, + version: '2.0.1', + author: 'GeoTech Labs', + createdAt: Date.now() - 40 * 24 * 60 * 60 * 1000, + updatedAt: Date.now() - 5 * 24 * 60 * 60 * 1000, + prompt: "You have access to amap servies which allows you to access the map information.", + modelId: "openai/gpt-4o", + mcpServers: { + "amap-mcp-server": { + url: "http://0.0.0.0:8000/sse" + } + } }, ]; diff --git a/src/features/cap/types/index.ts b/src/features/cap/types/index.ts index b64852f2..1348015a 100644 --- a/src/features/cap/types/index.ts +++ b/src/features/cap/types/index.ts @@ -3,7 +3,11 @@ interface CapData { prompt: string; modelId:string; - mcpUrl:string[]; + mcpServers: { + [name: string]: { + url: string; + } + }; } // Remote Cap Interface From 6797950009c6115681e5bc592536458809e91a8b Mon Sep 17 00:00:00 2001 From: Mine77 Date: Wed, 23 Jul 2025 19:05:54 +0800 Subject: [PATCH 05/85] refactor: remove artifact feature --- src/features/ai-chat/components/chat.tsx | 1 - src/features/ai-chat/components/message.tsx | 40 +- src/features/ai-chat/hooks/index.ts | 1 - .../ai-chat/services/prompts/index.ts | 30 +- .../ai-chat/services/tools/create-document.ts | 131 ----- src/features/ai-chat/services/tools/index.ts | 8 - .../ai-chat/services/tools/update-document.ts | 134 ----- src/features/ai-provider/services/provider.ts | 4 - .../documents/artifacts/code/actions/copy.tsx | 22 - .../documents/artifacts/code/actions/redo.tsx | 23 - .../artifacts/code/actions/run-code.tsx | 157 ------ .../documents/artifacts/code/actions/undo.tsx | 23 - .../code/components/code-content.tsx | 28 -- .../artifacts/code/components/code-editor.tsx | 110 ---- .../code/components/code-preview.tsx | 26 - .../artifacts/code/components/console.tsx | 180 ------- .../documents/artifacts/code/generator.ts | 54 -- .../documents/artifacts/code/index.tsx | 34 -- .../documents/artifacts/code/updater.ts | 35 -- .../artifacts/image/actions/copy.tsx | 33 -- .../artifacts/image/actions/redo.tsx | 16 - .../artifacts/image/actions/undo.tsx | 16 - .../image/components/image-content.tsx | 6 - .../image/components/image-editor.tsx | 48 -- .../image/components/image-preview.tsx | 20 - .../documents/artifacts/image/generator.ts | 17 - .../documents/artifacts/image/index.tsx | 19 - .../documents/artifacts/image/updater.ts | 17 - src/features/documents/artifacts/index.ts | 33 -- .../artifacts/sheet/actions/copy.tsx | 26 - .../artifacts/sheet/actions/redo.tsx | 18 - .../artifacts/sheet/actions/undo.tsx | 18 - .../sheet/components/sheet-content.tsx | 16 - .../sheet/components/sheet-editor.tsx | 143 ------ .../sheet/components/sheet-preview.tsx | 26 - .../documents/artifacts/sheet/generator.ts | 32 -- .../documents/artifacts/sheet/index.tsx | 22 - .../documents/artifacts/sheet/updater.ts | 36 -- .../documents/artifacts/text/actions/copy.tsx | 18 - .../documents/artifacts/text/actions/redo.tsx | 16 - .../documents/artifacts/text/actions/undo.tsx | 16 - .../artifacts/text/actions/version-change.tsx | 16 - .../artifacts/text/components/diffview.tsx | 95 ---- .../text/components/editor/code-block.tsx | 38 -- .../text/components/editor/config.ts | 47 -- .../artifacts/text/components/editor/diff.js | 475 ------------------ .../text/components/editor/functions.tsx | 19 - .../text/components/editor/markdown.css | 13 - .../text/components/editor/markdown.tsx | 22 - .../text/components/editor/react-renderer.tsx | 12 - .../text/components/text-content.tsx | 52 -- .../artifacts/text/components/text-editor.tsx | 129 ----- .../text/components/text-preview.tsx | 20 - .../documents/artifacts/text/generator.ts | 27 - .../documents/artifacts/text/index.tsx | 26 - .../documents/artifacts/text/updater.ts | 42 -- src/features/documents/artifacts/types.tsx | 77 --- .../documents/components/artifact-actions.tsx | 108 ---- .../components/artifact-close-button.tsx | 26 - .../components/artifact-messages-header.tsx | 32 -- .../documents/components/artifact-viewer.tsx | 253 ---------- .../documents/components/artifact.tsx | 108 ---- .../components/document-preview-call.tsx | 83 --- .../components/document-preview-result.tsx | 86 ---- .../documents/components/document-preview.tsx | 262 ---------- src/features/documents/components/index.ts | 8 - .../documents/components/version-footer.tsx | 76 --- src/features/documents/hooks/index.ts | 4 - .../documents/hooks/use-document-current.ts | 59 --- src/features/documents/hooks/use-document.ts | 42 -- src/features/documents/hooks/use-documents.ts | 108 ---- .../documents/hooks/use-version-management.ts | 49 -- src/features/documents/stores/index.ts | 383 -------------- src/features/documents/types/index.ts | 18 - src/layout/components/app-sidebar.tsx | 10 +- src/pages/artifact.tsx | 21 - src/pages/index.ts | 1 - src/router.tsx | 2 - src/shared/hooks/use-storage.ts | 4 - src/storage/actions.ts | 2 - src/storage/db.ts | 2 - 81 files changed, 5 insertions(+), 4505 deletions(-) delete mode 100644 src/features/ai-chat/services/tools/create-document.ts delete mode 100644 src/features/ai-chat/services/tools/update-document.ts delete mode 100644 src/features/documents/artifacts/code/actions/copy.tsx delete mode 100644 src/features/documents/artifacts/code/actions/redo.tsx delete mode 100644 src/features/documents/artifacts/code/actions/run-code.tsx delete mode 100644 src/features/documents/artifacts/code/actions/undo.tsx delete mode 100644 src/features/documents/artifacts/code/components/code-content.tsx delete mode 100644 src/features/documents/artifacts/code/components/code-editor.tsx delete mode 100644 src/features/documents/artifacts/code/components/code-preview.tsx delete mode 100644 src/features/documents/artifacts/code/components/console.tsx delete mode 100644 src/features/documents/artifacts/code/generator.ts delete mode 100644 src/features/documents/artifacts/code/index.tsx delete mode 100644 src/features/documents/artifacts/code/updater.ts delete mode 100644 src/features/documents/artifacts/image/actions/copy.tsx delete mode 100644 src/features/documents/artifacts/image/actions/redo.tsx delete mode 100644 src/features/documents/artifacts/image/actions/undo.tsx delete mode 100644 src/features/documents/artifacts/image/components/image-content.tsx delete mode 100644 src/features/documents/artifacts/image/components/image-editor.tsx delete mode 100644 src/features/documents/artifacts/image/components/image-preview.tsx delete mode 100644 src/features/documents/artifacts/image/generator.ts delete mode 100644 src/features/documents/artifacts/image/index.tsx delete mode 100644 src/features/documents/artifacts/image/updater.ts delete mode 100644 src/features/documents/artifacts/index.ts delete mode 100644 src/features/documents/artifacts/sheet/actions/copy.tsx delete mode 100644 src/features/documents/artifacts/sheet/actions/redo.tsx delete mode 100644 src/features/documents/artifacts/sheet/actions/undo.tsx delete mode 100644 src/features/documents/artifacts/sheet/components/sheet-content.tsx delete mode 100644 src/features/documents/artifacts/sheet/components/sheet-editor.tsx delete mode 100644 src/features/documents/artifacts/sheet/components/sheet-preview.tsx delete mode 100644 src/features/documents/artifacts/sheet/generator.ts delete mode 100644 src/features/documents/artifacts/sheet/index.tsx delete mode 100644 src/features/documents/artifacts/sheet/updater.ts delete mode 100644 src/features/documents/artifacts/text/actions/copy.tsx delete mode 100644 src/features/documents/artifacts/text/actions/redo.tsx delete mode 100644 src/features/documents/artifacts/text/actions/undo.tsx delete mode 100644 src/features/documents/artifacts/text/actions/version-change.tsx delete mode 100644 src/features/documents/artifacts/text/components/diffview.tsx delete mode 100644 src/features/documents/artifacts/text/components/editor/code-block.tsx delete mode 100644 src/features/documents/artifacts/text/components/editor/config.ts delete mode 100644 src/features/documents/artifacts/text/components/editor/diff.js delete mode 100644 src/features/documents/artifacts/text/components/editor/functions.tsx delete mode 100644 src/features/documents/artifacts/text/components/editor/markdown.css delete mode 100644 src/features/documents/artifacts/text/components/editor/markdown.tsx delete mode 100644 src/features/documents/artifacts/text/components/editor/react-renderer.tsx delete mode 100644 src/features/documents/artifacts/text/components/text-content.tsx delete mode 100644 src/features/documents/artifacts/text/components/text-editor.tsx delete mode 100644 src/features/documents/artifacts/text/components/text-preview.tsx delete mode 100644 src/features/documents/artifacts/text/generator.ts delete mode 100644 src/features/documents/artifacts/text/index.tsx delete mode 100644 src/features/documents/artifacts/text/updater.ts delete mode 100644 src/features/documents/artifacts/types.tsx delete mode 100644 src/features/documents/components/artifact-actions.tsx delete mode 100644 src/features/documents/components/artifact-close-button.tsx delete mode 100644 src/features/documents/components/artifact-messages-header.tsx delete mode 100644 src/features/documents/components/artifact-viewer.tsx delete mode 100644 src/features/documents/components/artifact.tsx delete mode 100644 src/features/documents/components/document-preview-call.tsx delete mode 100644 src/features/documents/components/document-preview-result.tsx delete mode 100644 src/features/documents/components/document-preview.tsx delete mode 100644 src/features/documents/components/index.ts delete mode 100644 src/features/documents/components/version-footer.tsx delete mode 100644 src/features/documents/hooks/index.ts delete mode 100644 src/features/documents/hooks/use-document-current.ts delete mode 100644 src/features/documents/hooks/use-document.ts delete mode 100644 src/features/documents/hooks/use-documents.ts delete mode 100644 src/features/documents/hooks/use-version-management.ts delete mode 100644 src/features/documents/stores/index.ts delete mode 100644 src/features/documents/types/index.ts delete mode 100644 src/pages/artifact.tsx diff --git a/src/features/ai-chat/components/chat.tsx b/src/features/ai-chat/components/chat.tsx index cb080ae6..4c830d85 100644 --- a/src/features/ai-chat/components/chat.tsx +++ b/src/features/ai-chat/components/chat.tsx @@ -33,7 +33,6 @@ export function Chat({ return (
- {/* Artifact viewer */} {/* Chat */}
diff --git a/src/features/ai-chat/components/message.tsx b/src/features/ai-chat/components/message.tsx index 19e9b1fb..4c1f45de 100644 --- a/src/features/ai-chat/components/message.tsx +++ b/src/features/ai-chat/components/message.tsx @@ -1,4 +1,4 @@ -'use client'; +'use client';; import type { UseChatHelpers } from '@ai-sdk/react'; import type { UIMessage } from 'ai'; @@ -6,11 +6,7 @@ import cx from 'classnames'; import equal from 'fast-deep-equal'; import { AnimatePresence, motion } from 'framer-motion'; import { memo, useState } from 'react'; -import { DocumentPreview } from '@/features/documents/components/document-preview'; -import { DocumentToolCall } from '@/features/documents/components/document-preview-call'; -import { DocumentToolResult } from '@/features/documents/components/document-preview-result'; import { cn, generateUUID } from '@/shared/utils'; -import { MemoryToolCall } from './memory-tool-call'; import { MessageActions } from './message-actions'; import { MessageReasoning } from './message-reasoning'; import { MessageSource } from './message-source'; @@ -124,22 +120,7 @@ const PurePreviewMessage = ({ skeleton: ['getWeather'].includes(toolName), })} > - {toolName === 'createDocument' ? ( - - ) : toolName === 'updateDocument' ? ( - - ) : ['saveMemory', 'queryMemory'].includes(toolName) ? ( - - ) : null} + {`${toolName} is called`}
); } @@ -149,22 +130,7 @@ const PurePreviewMessage = ({ return (
- {toolName === 'createDocument' ? ( - - ) : toolName === 'updateDocument' ? ( - - ) : ['saveMemory', 'queryMemory'].includes(toolName) ? ( - - ) : null} + {`${toolName} result is ${result}`}
); } diff --git a/src/features/ai-chat/hooks/index.ts b/src/features/ai-chat/hooks/index.ts index 19ffc1c5..a35660fb 100644 --- a/src/features/ai-chat/hooks/index.ts +++ b/src/features/ai-chat/hooks/index.ts @@ -2,7 +2,6 @@ export * from '../../settings/hooks/use-memory'; export * from './use-chat-default'; export * from './use-chat-page'; export * from './use-chat-sessions'; -export * from './use-chat-streams'; export * from './use-file'; export * from './use-files'; export * from './use-messages-ui'; diff --git a/src/features/ai-chat/services/prompts/index.ts b/src/features/ai-chat/services/prompts/index.ts index ad5d6924..3963ee39 100644 --- a/src/features/ai-chat/services/prompts/index.ts +++ b/src/features/ai-chat/services/prompts/index.ts @@ -1,31 +1,3 @@ -export const artifactsPrompt = ` -## Artifacts -Artifacts is a special user interface mode that helps users with writing, editing, and other content creation tasks. When artifact is open, it is on the right side of the screen, while the conversation is on the left side. When creating or updating documents, changes are reflected in real-time on the artifacts and visible to the user. - -DO NOT UPDATE DOCUMENTS IMMEDIATELY AFTER CREATING THEM. WAIT FOR USER FEEDBACK OR REQUEST TO UPDATE IT. - -This is a guide for using artifacts tools: \`createDocument\` and \`updateDocument\`, which render content on a artifacts beside the conversation. - -**When to use \`createDocument\`:** -- For substantial content (>10 lines) -- For content users will likely save/reuse (emails, essays, etc.) -- When explicitly requested to create a document - -**When NOT to use \`createDocument\`:** -- For informational/explanatory content -- For conversational responses -- When asked to keep it in chat - -**Using \`updateDocument\`:** -- Default to full document rewrites for major changes -- Use targeted updates only for specific, isolated changes -- Follow user instructions for which parts to modify - -**When NOT to use \`updateDocument\`:** -- Immediately after creating a document - -Do not update document right after creating it. Wait for user feedback or request to update it. -`; const memoryPrompt = ` ## Memory Management @@ -61,5 +33,5 @@ export const systemPrompt = () => { }; export const devModeSystemPrompt = () => { - return `${regularPrompt}\n\n${memoryPrompt}\n\n${artifactsPrompt}`; + return `${regularPrompt}\n\n${memoryPrompt}`; }; diff --git a/src/features/ai-chat/services/tools/create-document.ts b/src/features/ai-chat/services/tools/create-document.ts deleted file mode 100644 index 48ef4a7c..00000000 --- a/src/features/ai-chat/services/tools/create-document.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { tool } from 'ai'; -import { z } from 'zod'; -import { generateCodeContent } from '@/features/documents/artifacts/code'; -import { generateImageContent } from '@/features/documents/artifacts/image'; -import { generateSheetContent } from '@/features/documents/artifacts/sheet'; -import { generateTextContent } from '@/features/documents/artifacts/text'; -import { DocumentStateStore } from '@/features/documents/stores'; -import { generateUUID } from '@/shared/utils'; - -// const artifactKinds = ['text', 'code', 'image', 'sheet'] as const; -const artifactKinds = ['text'] as const; - -// generate function mapping -const generators = { - text: generateTextContent, - code: generateCodeContent, - sheet: generateSheetContent, - image: generateImageContent, -}; - -export const createDocument = () => - tool({ - description: - 'Create a document for a writing or content creation activities. This will generate content using AI and save it locally.', - parameters: z.object({ - title: z.string(), - kind: z.enum(artifactKinds), - }), - execute: async ({ title, kind }) => { - const id = generateUUID(); - const { setCurrentDocument } = DocumentStateStore.getState(); - - try { - // create initial document - const { createDocumentWithId, setDocumentContent } = - DocumentStateStore.getState(); - createDocumentWithId(id, title, kind); - - // set artifact to streaming state - setCurrentDocument((artifact) => ({ - ...artifact, - documentId: id, - title, - kind, - content: '', - status: 'streaming', - })); - - // get the corresponding generator - const generator = generators[kind]; - if (!generator) { - throw new Error(`No generator found for kind: ${kind}`); - } - - let finalContent = ''; - - // call the AI generate function, update artifact content in real time - if (kind === 'text') { - finalContent = await (generator as typeof generateTextContent)( - title, - (delta) => { - setCurrentDocument((artifact) => ({ - ...artifact, - content: artifact.content + delta, - status: 'streaming', - })); - }, - ); - } else if (kind === 'code') { - finalContent = await (generator as typeof generateCodeContent)( - title, - (delta) => { - setCurrentDocument((artifact) => ({ - ...artifact, - content: delta, - status: 'streaming', - })); - }, - ); - } else if (kind === 'sheet') { - finalContent = await (generator as typeof generateSheetContent)( - title, - (delta) => { - setCurrentDocument((artifact) => ({ - ...artifact, - content: delta, - status: 'streaming', - })); - }, - ); - } else if (kind === 'image') { - finalContent = await (generator as typeof generateImageContent)( - title, - (imageBase64) => { - setCurrentDocument((artifact) => ({ - ...artifact, - content: imageBase64, - status: 'streaming', - })); - }, - ); - } - - // update document content and set artifact to idle state - setDocumentContent(id, finalContent); - - setCurrentDocument((artifact) => ({ - ...artifact, - content: finalContent, - status: 'idle', - })); - - return { - id, - title, - kind, - content: finalContent, - message: `A ${kind} document "${title}" has been created and saved locally.`, - }; - } catch (error) { - console.error('Failed to create document:', error); - // delete failed document and reset artifact state - DocumentStateStore.getState().deleteDocument(id); - setCurrentDocument((artifact) => ({ - ...artifact, - status: 'idle', - })); - throw error; - } - }, - }); diff --git a/src/features/ai-chat/services/tools/index.ts b/src/features/ai-chat/services/tools/index.ts index 89208fe2..d9d918dc 100644 --- a/src/features/ai-chat/services/tools/index.ts +++ b/src/features/ai-chat/services/tools/index.ts @@ -1,22 +1,14 @@ import { ModelStateStore } from '@/features/ai-provider/stores'; import { SettingsStateStore } from '@/features/settings/stores'; -import { createDocument } from '../tools/create-document'; import { queryMemory, saveMemory } from '../tools/memory'; -import { updateDocument } from '../tools/update-document'; const selectedModel = ModelStateStore.getState().selectedModel; const isDevMode = SettingsStateStore.getState().settings.devMode; const userModeTools = { - // createDocument: createDocument(), - // updateDocument: updateDocument(), - // saveMemory: saveMemory(), - // queryMemory: queryMemory(), }; const devModeTools = { - createDocument: createDocument(), - updateDocument: updateDocument(), saveMemory: saveMemory(), queryMemory: queryMemory(), }; diff --git a/src/features/ai-chat/services/tools/update-document.ts b/src/features/ai-chat/services/tools/update-document.ts deleted file mode 100644 index 533a90a6..00000000 --- a/src/features/ai-chat/services/tools/update-document.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { tool } from 'ai'; -import { z } from 'zod'; -import { updateCodeContent } from '@/features/documents/artifacts/code'; -import { updateImageContent } from '@/features/documents/artifacts/image'; -import { updateSheetContent } from '@/features/documents/artifacts/sheet'; -import { updateTextContent } from '@/features/documents/artifacts/text'; -import { DocumentStateStore } from '@/features/documents/stores'; - -// update function mapping -const updaters = { - text: updateTextContent, - code: updateCodeContent, - sheet: updateSheetContent, - image: updateImageContent, -}; - -export const updateDocument = () => - tool({ - description: 'Update a document with the given description using AI.', - parameters: z.object({ - id: z.string().describe('The ID of the document to update'), - description: z - .string() - .describe('The description of changes that need to be made'), - }), - execute: async ({ id, description }) => { - const { setCurrentDocument, getDocument, addNewVersionDocument } = - DocumentStateStore.getState(); - try { - // Get document from client store - const document = getDocument(id); - - if (!document) { - return { - error: 'Document not found', - }; - } - - // set artifact to streaming state - setCurrentDocument((artifact) => ({ - ...artifact, - documentId: id, - title: document.title, - kind: document.kind, - content: document.content || '', - status: 'streaming', - })); - - // get the corresponding updater - const updater = updaters[document.kind]; - if (!updater) { - throw new Error(`No updater found for kind: ${document.kind}`); - } - - let updatedContent = ''; - - // call the AI update function, update artifact content in real time - if (document.kind === 'text') { - updatedContent = await (updater as typeof updateTextContent)( - document.content || '', - description, - (delta) => { - setCurrentDocument((artifact) => ({ - ...artifact, - content: artifact.content + delta, - status: 'streaming', - })); - }, - ); - } else if (document.kind === 'code') { - updatedContent = await (updater as typeof updateCodeContent)( - document.content || '', - description, - (delta) => { - setCurrentDocument((artifact) => ({ - ...artifact, - content: delta, - status: 'streaming', - })); - }, - ); - } else if (document.kind === 'sheet') { - updatedContent = await (updater as typeof updateSheetContent)( - document.content || '', - description, - (delta) => { - setCurrentDocument((artifact) => ({ - ...artifact, - content: delta, - status: 'streaming', - })); - }, - ); - } else if (document.kind === 'image') { - // image update does not need current content - updatedContent = await (updater as typeof updateImageContent)( - description, - (imageBase64) => { - setCurrentDocument((artifact) => ({ - ...artifact, - content: imageBase64, - status: 'streaming', - })); - }, - ); - } - - // update document content and set artifact to idle state - // setDocumentContent(id, updatedContent); - addNewVersionDocument(id, updatedContent); - - setCurrentDocument((artifact) => ({ - ...artifact, - content: updatedContent, - status: 'idle', - })); - - return { - id, - title: document.title, - kind: document.kind, - content: updatedContent, - message: `The ${document.kind} document "${document.title}" has been updated successfully.`, - }; - } catch (error) { - console.error('Failed to update document:', error); - setCurrentDocument((artifact) => ({ - ...artifact, - status: 'idle', - })); - throw error; - } - }, - }); diff --git a/src/features/ai-provider/services/provider.ts b/src/features/ai-provider/services/provider.ts index 3b64af8b..6533e436 100644 --- a/src/features/ai-provider/services/provider.ts +++ b/src/features/ai-provider/services/provider.ts @@ -26,10 +26,6 @@ export const llmProvider = { }, }); }, - artifact: () => { - const selectedModel = ModelStateStore.getState().selectedModel; - return openrouter.chat(selectedModel.id); - }, utility: () => { const selectedModel = ModelStateStore.getState().selectedModel; return openrouter.chat('openai/gpt-4o-mini'); diff --git a/src/features/documents/artifacts/code/actions/copy.tsx b/src/features/documents/artifacts/code/actions/copy.tsx deleted file mode 100644 index accf79c2..00000000 --- a/src/features/documents/artifacts/code/actions/copy.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { CopyIcon } from 'lucide-react'; -import { toast } from '@/shared/components/toast'; -import { getLocaleText } from '@/shared/locales'; -import type { ArtifactAction } from '../../types'; - -interface Metadata { - outputs: Array; -} - -export function createCopyAction(): ArtifactAction { - return { - icon: , - description: getLocaleText('en').t('artifact.code.actions.copy'), - onClick: ({ content }) => { - navigator.clipboard.writeText(content); - toast({ - description: getLocaleText('en').t('artifact.copied'), - type: 'success', - }); - }, - }; -} diff --git a/src/features/documents/artifacts/code/actions/redo.tsx b/src/features/documents/artifacts/code/actions/redo.tsx deleted file mode 100644 index b0d8802b..00000000 --- a/src/features/documents/artifacts/code/actions/redo.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { RedoIcon } from 'lucide-react'; -import { getLocaleText } from '@/shared/locales'; -import type { ArtifactAction } from '../../types'; - -interface Metadata { - outputs: Array; -} - -export function createRedoAction(): ArtifactAction { - return { - icon: , - description: getLocaleText('en').t('artifact.code.actions.redo'), - onClick: ({ handleVersionChange }) => { - handleVersionChange('next'); - }, - isDisabled: ({ isCurrentVersion }) => { - if (isCurrentVersion) { - return true; - } - return false; - }, - }; -} diff --git a/src/features/documents/artifacts/code/actions/run-code.tsx b/src/features/documents/artifacts/code/actions/run-code.tsx deleted file mode 100644 index b9905451..00000000 --- a/src/features/documents/artifacts/code/actions/run-code.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import { PlayIcon } from 'lucide-react'; -import { getLocaleText } from '@/shared/locales'; -import { generateUUID } from '@/shared/utils'; -import type { ArtifactAction } from '../../types'; -import type { - ConsoleOutput, - ConsoleOutputContent, -} from '../components/console'; - -interface Metadata { - outputs: Array; -} - -const OUTPUT_HANDLERS = { - matplotlib: ` - import io - import base64 - from matplotlib import pyplot as plt - - # Clear any existing plots - plt.clf() - plt.close('all') - - # Switch to agg backend - plt.switch_backend('agg') - - def setup_matplotlib_output(): - def custom_show(): - if plt.gcf().get_size_inches().prod() * plt.gcf().dpi ** 2 > 25_000_000: - print("Warning: Plot size too large, reducing quality") - plt.gcf().set_dpi(100) - - png_buf = io.BytesIO() - plt.savefig(png_buf, format='png') - png_buf.seek(0) - png_base64 = base64.b64encode(png_buf.read()).decode('utf-8') - print(f'data:image/png;base64,{png_base64}') - png_buf.close() - - plt.clf() - plt.close('all') - - plt.show = custom_show - `, - basic: ` - # Basic output capture setup - `, -}; - -function detectRequiredHandlers(code: string): string[] { - const handlers: string[] = ['basic']; - - if (code.includes('matplotlib') || code.includes('plt.')) { - handlers.push('matplotlib'); - } - - return handlers; -} - -export function createRunCodeAction(): ArtifactAction { - return { - icon: , - label: undefined as any, - description: getLocaleText('en').t('artifact.code.actions.run'), - onClick: async ({ content, setMetadata }) => { - const runId = generateUUID(); - const outputContent: Array = []; - - setMetadata((metadata) => ({ - ...metadata, - outputs: [ - ...metadata.outputs, - { - id: runId, - contents: [], - status: 'in_progress', - }, - ], - })); - - try { - // @ts-expect-error - loadPyodide is not defined - const currentPyodideInstance = await globalThis.loadPyodide({ - indexURL: 'https://cdn.jsdelivr.net/pyodide/v0.23.4/full/', - }); - - currentPyodideInstance.setStdout({ - batched: (output: string) => { - outputContent.push({ - type: output.startsWith('data:image/png;base64') - ? 'image' - : 'text', - value: output, - }); - }, - }); - - await currentPyodideInstance.loadPackagesFromImports(content, { - messageCallback: (message: string) => { - setMetadata((metadata) => ({ - ...metadata, - outputs: [ - ...metadata.outputs.filter((output) => output.id !== runId), - { - id: runId, - contents: [{ type: 'text', value: message }], - status: 'loading_packages', - }, - ], - })); - }, - }); - - const requiredHandlers = detectRequiredHandlers(content); - for (const handler of requiredHandlers) { - if (OUTPUT_HANDLERS[handler as keyof typeof OUTPUT_HANDLERS]) { - await currentPyodideInstance.runPythonAsync( - OUTPUT_HANDLERS[handler as keyof typeof OUTPUT_HANDLERS], - ); - - if (handler === 'matplotlib') { - await currentPyodideInstance.runPythonAsync( - 'setup_matplotlib_output()', - ); - } - } - } - - await currentPyodideInstance.runPythonAsync(content); - - setMetadata((metadata) => ({ - ...metadata, - outputs: [ - ...metadata.outputs.filter((output) => output.id !== runId), - { - id: runId, - contents: outputContent, - status: 'completed', - }, - ], - })); - } catch (error: any) { - setMetadata((metadata) => ({ - ...metadata, - outputs: [ - ...metadata.outputs.filter((output) => output.id !== runId), - { - id: runId, - contents: [{ type: 'text', value: error.message }], - status: 'failed', - }, - ], - })); - } - }, - }; -} diff --git a/src/features/documents/artifacts/code/actions/undo.tsx b/src/features/documents/artifacts/code/actions/undo.tsx deleted file mode 100644 index 1a0940d1..00000000 --- a/src/features/documents/artifacts/code/actions/undo.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { UndoIcon } from 'lucide-react'; -import { getLocaleText } from '@/shared/locales'; -import type { ArtifactAction } from '../../types'; - -interface Metadata { - outputs: Array; -} - -export function createUndoAction(): ArtifactAction { - return { - icon: , - description: getLocaleText('en').t('artifact.code.actions.undo'), - onClick: ({ handleVersionChange }) => { - handleVersionChange('prev'); - }, - isDisabled: ({ currentVersionIndex }) => { - if (currentVersionIndex === 0) { - return true; - } - return false; - }, - }; -} diff --git a/src/features/documents/artifacts/code/components/code-content.tsx b/src/features/documents/artifacts/code/components/code-content.tsx deleted file mode 100644 index 32d0285c..00000000 --- a/src/features/documents/artifacts/code/components/code-content.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import type { ArtifactContent } from '@/features/documents/artifacts/types'; -import { CodeEditor } from './code-editor'; -import { Console, type ConsoleOutput } from './console'; - -interface Metadata { - outputs: Array; -} - -export function CodeContent(props: ArtifactContent) { - return ( - <> -
- -
- {props.metadata?.outputs && ( - { - props.setMetadata({ - ...props.metadata, - outputs: [], - }); - }} - /> - )} - - ); -} diff --git a/src/features/documents/artifacts/code/components/code-editor.tsx b/src/features/documents/artifacts/code/components/code-editor.tsx deleted file mode 100644 index 49c34332..00000000 --- a/src/features/documents/artifacts/code/components/code-editor.tsx +++ /dev/null @@ -1,110 +0,0 @@ -'use client'; - -import { python } from '@codemirror/lang-python'; -import { EditorState, Transaction } from '@codemirror/state'; -import { oneDark } from '@codemirror/theme-one-dark'; -import { EditorView } from '@codemirror/view'; -import { basicSetup } from 'codemirror'; -import { memo, useEffect, useRef } from 'react'; - -type EditorProps = { - content: string; - onSaveContent: (updatedContent: string, debounce: boolean) => void; - status: 'streaming' | 'idle'; - isCurrentVersion: boolean; - currentVersionIndex: number; -}; - -function PureCodeEditor({ content, onSaveContent, status }: EditorProps) { - const containerRef = useRef(null); - const editorRef = useRef(null); - - useEffect(() => { - if (containerRef.current && !editorRef.current) { - const startState = EditorState.create({ - doc: content, - extensions: [basicSetup, python(), oneDark], - }); - - editorRef.current = new EditorView({ - state: startState, - parent: containerRef.current, - }); - } - - return () => { - if (editorRef.current) { - editorRef.current.destroy(); - editorRef.current = null; - } - }; - // NOTE: we only want to run this effect once - // eslint-disable-next-line - }, []); - - useEffect(() => { - if (editorRef.current) { - const updateListener = EditorView.updateListener.of((update) => { - if (update.docChanged) { - const transaction = update.transactions.find( - (tr) => !tr.annotation(Transaction.remote), - ); - - if (transaction) { - const newContent = update.state.doc.toString(); - onSaveContent(newContent, true); - } - } - }); - - const currentSelection = editorRef.current.state.selection; - - const newState = EditorState.create({ - doc: editorRef.current.state.doc, - extensions: [basicSetup, python(), oneDark, updateListener], - selection: currentSelection, - }); - - editorRef.current.setState(newState); - } - }, [onSaveContent]); - - useEffect(() => { - if (editorRef.current && content) { - const currentContent = editorRef.current.state.doc.toString(); - - if (status === 'streaming' || currentContent !== content) { - const transaction = editorRef.current.state.update({ - changes: { - from: 0, - to: currentContent.length, - insert: content, - }, - annotations: [Transaction.remote.of(true)], - }); - - editorRef.current.dispatch(transaction); - } - } - }, [content, status]); - - return ( -
- ); -} - -function areEqual(prevProps: EditorProps, nextProps: EditorProps) { - if (prevProps.currentVersionIndex !== nextProps.currentVersionIndex) - return false; - if (prevProps.isCurrentVersion !== nextProps.isCurrentVersion) return false; - if (prevProps.status === 'streaming' && nextProps.status === 'streaming') - return false; - if (prevProps.content !== nextProps.content) return false; - - return true; -} - -export const CodeEditor = memo(PureCodeEditor, areEqual); diff --git a/src/features/documents/artifacts/code/components/code-preview.tsx b/src/features/documents/artifacts/code/components/code-preview.tsx deleted file mode 100644 index 34cd2ba7..00000000 --- a/src/features/documents/artifacts/code/components/code-preview.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import type { Document } from '@/features/documents/stores'; -import { CodeEditor } from './code-editor'; - -interface CodePreviewProps { - document: Document; - editorStatus: 'streaming' | 'idle'; -} - -export function CodePreview({ document, editorStatus }: CodePreviewProps) { - const commonProps = { - content: document.content ?? '', - isCurrentVersion: true, - currentVersionIndex: 0, - status: editorStatus, - saveContent: () => {}, - suggestions: [], - }; - - return ( -
-
- {}} /> -
-
- ); -} diff --git a/src/features/documents/artifacts/code/components/console.tsx b/src/features/documents/artifacts/code/components/console.tsx deleted file mode 100644 index c2c74b76..00000000 --- a/src/features/documents/artifacts/code/components/console.tsx +++ /dev/null @@ -1,180 +0,0 @@ -import { CrossIcon, LoaderIcon, TerminalIcon } from 'lucide-react'; -import { - type Dispatch, - type SetStateAction, - useCallback, - useEffect, - useRef, - useState, -} from 'react'; -import { useCurrentDocument } from '@/features/documents/hooks/use-document-current'; -import { Button } from '@/shared/components/ui/button'; -import { cn } from '@/shared/utils'; - -export interface ConsoleOutputContent { - type: 'text' | 'image'; - value: string; -} - -export interface ConsoleOutput { - id: string; - status: 'in_progress' | 'loading_packages' | 'completed' | 'failed'; - contents: Array; -} - -interface ConsoleProps { - consoleOutputs: Array; - setConsoleOutputs: Dispatch>>; -} - -export function Console({ consoleOutputs, setConsoleOutputs }: ConsoleProps) { - const [height, setHeight] = useState(300); - const [isResizing, setIsResizing] = useState(false); - const consoleEndRef = useRef(null); - - const { currentDocument } = useCurrentDocument(); - const isArtifactVisible = currentDocument.documentId !== 'init'; - - const minHeight = 100; - const maxHeight = 800; - - const startResizing = useCallback(() => { - setIsResizing(true); - }, []); - - const stopResizing = useCallback(() => { - setIsResizing(false); - }, []); - - const resize = useCallback( - (e: MouseEvent) => { - if (isResizing) { - const newHeight = window.innerHeight - e.clientY; - if (newHeight >= minHeight && newHeight <= maxHeight) { - setHeight(newHeight); - } - } - }, - [isResizing], - ); - - useEffect(() => { - window.addEventListener('mousemove', resize); - window.addEventListener('mouseup', stopResizing); - return () => { - window.removeEventListener('mousemove', resize); - window.removeEventListener('mouseup', stopResizing); - }; - }, [resize, stopResizing]); - - useEffect(() => { - consoleEndRef.current?.scrollIntoView({ behavior: 'smooth' }); - }, [consoleOutputs]); - - useEffect(() => { - if (!isArtifactVisible) { - setConsoleOutputs([]); - } - }, [isArtifactVisible, setConsoleOutputs]); - - return consoleOutputs.length > 0 ? ( - <> -
- -
-
-
-
- -
-
Console
-
- -
- -
- {consoleOutputs.map((consoleOutput, index) => ( -
-
- [{index + 1}] -
- {['in_progress', 'loading_packages'].includes( - consoleOutput.status, - ) ? ( -
-
- -
-
- {consoleOutput.status === 'in_progress' - ? 'Initializing...' - : consoleOutput.status === 'loading_packages' - ? consoleOutput.contents.map((content) => - content.type === 'text' ? content.value : null, - ) - : null} -
-
- ) : ( -
- {consoleOutput.contents.map((content, index) => - content.type === 'image' ? ( - - output - - ) : ( -
- {content.value} -
- ), - )} -
- )} -
- ))} -
-
-
- - ) : null; -} diff --git a/src/features/documents/artifacts/code/generator.ts b/src/features/documents/artifacts/code/generator.ts deleted file mode 100644 index 0ffbe7e2..00000000 --- a/src/features/documents/artifacts/code/generator.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { streamObject } from 'ai'; -import z from 'zod'; -import { llmProvider } from '@/features/ai-provider/services'; - -export const generateCodePrompt = ` -You are a Python code generator that creates self-contained, executable code snippets. When writing code: - -1. Each snippet should be complete and runnable on its own -2. Prefer using print() statements to display outputs -3. Include helpful comments explaining the code -4. Keep snippets concise (generally under 15 lines) -5. Avoid external dependencies - use Python standard library -6. Handle potential errors gracefully -7. Return meaningful output that demonstrates the code's functionality -8. Don't use input() or other interactive functions -9. Don't access files or network resources -10. Don't use infinite loops - -Examples of good snippets: - -# Calculate factorial iteratively -def factorial(n): - result = 1 - for i in range(1, n + 1): - result *= i - return result - -print(f"Factorial of 5 is: {factorial(5)}") -`; - -export async function generateCodeContent( - title: string, - onDelta: (delta: string) => void, -): Promise { - let draftContent = ''; - - const { fullStream } = streamObject({ - model: llmProvider.artifact(), - system: generateCodePrompt, - prompt: title, - schema: z.object({ - code: z.string(), - }), - }); - - for await (const delta of fullStream) { - if (delta.type === 'object' && delta.object.code) { - draftContent = delta.object.code; - onDelta(delta.object.code); - } - } - - return draftContent; -} diff --git a/src/features/documents/artifacts/code/index.tsx b/src/features/documents/artifacts/code/index.tsx deleted file mode 100644 index f4b057f8..00000000 --- a/src/features/documents/artifacts/code/index.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { Artifact } from '../types'; -import { createCopyAction } from './actions/copy'; -import { createRedoAction } from './actions/redo'; -import { createRunCodeAction } from './actions/run-code'; -import { createUndoAction } from './actions/undo'; -import { CodeContent } from './components/code-content'; -import type { ConsoleOutput } from './components/console'; - -// export functions for external use -export { generateCodeContent } from './generator'; -export { updateCodeContent } from './updater'; - -interface Metadata { - outputs: Array; -} - -export const createCodeArtifact = () => { - return new Artifact<'code', Metadata>({ - kind: 'code', - description: 'Code artifact for running and displaying code', - initialize: async ({ setMetadata }) => { - setMetadata({ outputs: [] }); - }, - content: CodeContent, - actions: [ - createRunCodeAction(), - createUndoAction(), - createRedoAction(), - createCopyAction(), - ], - }); -}; - -export const codeArtifact = createCodeArtifact(); diff --git a/src/features/documents/artifacts/code/updater.ts b/src/features/documents/artifacts/code/updater.ts deleted file mode 100644 index 8f7dc9d2..00000000 --- a/src/features/documents/artifacts/code/updater.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { streamObject } from 'ai'; -import { z } from 'zod'; -import { llmProvider } from '@/features/ai-provider/services'; - -export const updateCodePrompt = (currentContent: string) => ` - Improve the following code snippet based on the given prompt. - - ${currentContent} - `; - -export async function updateCodeContent( - currentContent: string, - description: string, - onDelta: (delta: string) => void, -): Promise { - let draftContent = ''; - - const { fullStream } = streamObject({ - model: llmProvider.artifact(), - system: updateCodePrompt(currentContent), - prompt: description, - schema: z.object({ - code: z.string(), - }), - }); - - for await (const delta of fullStream) { - if (delta.type === 'object' && delta.object.code) { - draftContent = delta.object.code; - onDelta(delta.object.code); - } - } - - return draftContent; -} diff --git a/src/features/documents/artifacts/image/actions/copy.tsx b/src/features/documents/artifacts/image/actions/copy.tsx deleted file mode 100644 index d5a7eaa1..00000000 --- a/src/features/documents/artifacts/image/actions/copy.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { CopyIcon } from 'lucide-react'; -import { toast } from '@/shared/components/toast'; -import { getLocaleText } from '@/shared/locales'; -import type { ArtifactAction } from '../../types'; - -export function createCopyAction(): ArtifactAction { - return { - icon: , - description: getLocaleText('en').t('artifact.image.actions.copy'), - onClick: ({ content }) => { - const img = new Image(); - img.src = `data:image/png;base64,${content}`; - img.onload = () => { - const canvas = document.createElement('canvas'); - canvas.width = img.width; - canvas.height = img.height; - const ctx = canvas.getContext('2d'); - ctx?.drawImage(img, 0, 0); - canvas.toBlob((blob) => { - if (blob) { - navigator.clipboard.write([ - new ClipboardItem({ 'image/png': blob }), - ]); - } - }, 'image/png'); - }; - toast({ - description: getLocaleText('en').t('artifact.image.copiedImage'), - type: 'success', - }); - }, - }; -} diff --git a/src/features/documents/artifacts/image/actions/redo.tsx b/src/features/documents/artifacts/image/actions/redo.tsx deleted file mode 100644 index a7546c98..00000000 --- a/src/features/documents/artifacts/image/actions/redo.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { RedoIcon } from 'lucide-react'; -import { getLocaleText } from '@/shared/locales'; -import type { ArtifactAction } from '../../types'; - -export function createRedoAction(): ArtifactAction { - return { - icon: , - description: getLocaleText('en').t('artifact.image.actions.redo'), - onClick: ({ handleVersionChange }) => { - handleVersionChange('next'); - }, - isDisabled: ({ isCurrentVersion }) => { - return isCurrentVersion; - }, - }; -} diff --git a/src/features/documents/artifacts/image/actions/undo.tsx b/src/features/documents/artifacts/image/actions/undo.tsx deleted file mode 100644 index 388b3a0a..00000000 --- a/src/features/documents/artifacts/image/actions/undo.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { UndoIcon } from 'lucide-react'; -import { getLocaleText } from '@/shared/locales'; -import type { ArtifactAction } from '../../types'; - -export function createUndoAction(): ArtifactAction { - return { - icon: , - description: getLocaleText('en').t('artifact.image.actions.undo'), - onClick: ({ handleVersionChange }) => { - handleVersionChange('prev'); - }, - isDisabled: ({ currentVersionIndex }) => { - return currentVersionIndex === 0; - }, - }; -} diff --git a/src/features/documents/artifacts/image/components/image-content.tsx b/src/features/documents/artifacts/image/components/image-content.tsx deleted file mode 100644 index bace8659..00000000 --- a/src/features/documents/artifacts/image/components/image-content.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import type { ArtifactContent } from '@/features/documents/artifacts/types'; -import { ImageEditor } from './image-editor'; - -export function ImageContent(props: ArtifactContent) { - return ; -} diff --git a/src/features/documents/artifacts/image/components/image-editor.tsx b/src/features/documents/artifacts/image/components/image-editor.tsx deleted file mode 100644 index 6fa4ad13..00000000 --- a/src/features/documents/artifacts/image/components/image-editor.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import cn from 'classnames'; -import { LoaderIcon } from 'lucide-react'; - -interface ImageEditorProps { - title: string; - content: string; - isCurrentVersion: boolean; - currentVersionIndex: number; - status: string; - isInline: boolean; -} - -export function ImageEditor({ - title, - content, - status, - isInline, -}: ImageEditorProps) { - return ( -
- {status === 'streaming' ? ( -
- {!isInline && ( -
- -
- )} -
Generating Image...
-
- ) : ( - - {title} - - )} -
- ); -} diff --git a/src/features/documents/artifacts/image/components/image-preview.tsx b/src/features/documents/artifacts/image/components/image-preview.tsx deleted file mode 100644 index ad117f33..00000000 --- a/src/features/documents/artifacts/image/components/image-preview.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import type { Document } from '@/features/documents/stores'; -import { ImageEditor } from './image-editor'; - -interface ImagePreviewProps { - document: Document; - artifactStatus: 'streaming' | 'idle' | 'loading' | 'error' | 'success'; -} - -export function ImagePreview({ document, artifactStatus }: ImagePreviewProps) { - return ( - - ); -} diff --git a/src/features/documents/artifacts/image/generator.ts b/src/features/documents/artifacts/image/generator.ts deleted file mode 100644 index 858a185d..00000000 --- a/src/features/documents/artifacts/image/generator.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { experimental_generateImage } from 'ai'; -import { llmProvider } from '@/features/ai-provider/services'; - -export async function generateImageContent( - title: string, - onComplete: (imageBase64: string) => void, -): Promise { - const { image } = await experimental_generateImage({ - model: llmProvider.image(), - prompt: title, - n: 1, - }); - - const base64Content = image.base64; - onComplete(base64Content); - return base64Content; -} diff --git a/src/features/documents/artifacts/image/index.tsx b/src/features/documents/artifacts/image/index.tsx deleted file mode 100644 index 8d7af6bf..00000000 --- a/src/features/documents/artifacts/image/index.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { Artifact } from '@/features/documents/artifacts/types'; -import { createCopyAction } from './actions/copy'; -import { createRedoAction } from './actions/redo'; -import { createUndoAction } from './actions/undo'; -import { ImageContent } from './components/image-content'; - -export const createImageArtifact = () => { - return new Artifact({ - kind: 'image', - description: 'Image artifact for displaying images', - content: ImageContent, - actions: [createUndoAction(), createRedoAction(), createCopyAction()], - }); -}; - -export const imageArtifact = createImageArtifact(); - -export { generateImageContent } from './generator'; -export { updateImageContent } from './updater'; diff --git a/src/features/documents/artifacts/image/updater.ts b/src/features/documents/artifacts/image/updater.ts deleted file mode 100644 index 37d85991..00000000 --- a/src/features/documents/artifacts/image/updater.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { experimental_generateImage } from 'ai'; -import { llmProvider } from '@/features/ai-provider/services'; - -export async function updateImageContent( - description: string, - onComplete: (imageBase64: string) => void, -): Promise { - const { image } = await experimental_generateImage({ - model: llmProvider.image(), - prompt: description, - n: 1, - }); - - const base64Content = image.base64; - onComplete(base64Content); - return base64Content; -} diff --git a/src/features/documents/artifacts/index.ts b/src/features/documents/artifacts/index.ts deleted file mode 100644 index 8f74e776..00000000 --- a/src/features/documents/artifacts/index.ts +++ /dev/null @@ -1,33 +0,0 @@ -// Export all artifact components - -export { codeArtifact } from './code'; -export { imageArtifact } from './image'; -export { sheetArtifact } from './sheet'; -export { textArtifact } from './text'; - -import { codeArtifact } from './code'; -import { CodePreview } from './code/components/code-preview'; -import { imageArtifact } from './image'; -import { ImagePreview } from './image/components/image-preview'; -import { sheetArtifact } from './sheet'; -import { SheetPreview } from './sheet/components/sheet-preview'; -// Export unified artifact definitions -import { textArtifact } from './text'; -// Export artifact previews -import { TextPreview } from './text/components/text-preview'; - -export const artifactDefinitions = [ - textArtifact, - codeArtifact, - imageArtifact, - sheetArtifact, -]; - -export type ArtifactKind = (typeof artifactDefinitions)[number]['kind']; - -export const artifactPreviews = { - text: TextPreview, - code: CodePreview, - image: ImagePreview, - sheet: SheetPreview, -}; diff --git a/src/features/documents/artifacts/sheet/actions/copy.tsx b/src/features/documents/artifacts/sheet/actions/copy.tsx deleted file mode 100644 index 04e95edc..00000000 --- a/src/features/documents/artifacts/sheet/actions/copy.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { CopyIcon } from 'lucide-react'; -import { parse, unparse } from 'papaparse'; -import { toast } from '@/shared/components/toast'; -import { getLocaleText } from '@/shared/locales'; -import type { ArtifactAction } from '../../types'; - -type Metadata = any; - -export function createCopyAction(): ArtifactAction { - return { - icon: , - description: getLocaleText('en').t('artifact.sheet.actions.copy'), - onClick: ({ content }) => { - const parsed = parse(content, { skipEmptyLines: true }); - const nonEmptyRows = parsed.data.filter((row) => - row.some((cell) => cell.trim() !== ''), - ); - const cleanedCsv = unparse(nonEmptyRows); - navigator.clipboard.writeText(cleanedCsv); - toast({ - description: getLocaleText('en').t('artifact.sheet.copiedCsv'), - type: 'success', - }); - }, - }; -} diff --git a/src/features/documents/artifacts/sheet/actions/redo.tsx b/src/features/documents/artifacts/sheet/actions/redo.tsx deleted file mode 100644 index 7de3965a..00000000 --- a/src/features/documents/artifacts/sheet/actions/redo.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { RedoIcon } from 'lucide-react'; -import { getLocaleText } from '@/shared/locales'; -import type { ArtifactAction } from '../../types'; - -type Metadata = any; - -export function createRedoAction(): ArtifactAction { - return { - icon: , - description: getLocaleText('en').t('artifact.sheet.actions.redo'), - onClick: ({ handleVersionChange }) => { - handleVersionChange('next'); - }, - isDisabled: ({ isCurrentVersion }) => { - return isCurrentVersion; - }, - }; -} diff --git a/src/features/documents/artifacts/sheet/actions/undo.tsx b/src/features/documents/artifacts/sheet/actions/undo.tsx deleted file mode 100644 index 43cafd2d..00000000 --- a/src/features/documents/artifacts/sheet/actions/undo.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { UndoIcon } from 'lucide-react'; -import { getLocaleText } from '@/shared/locales'; -import type { ArtifactAction } from '../../types'; - -type Metadata = any; - -export function createUndoAction(): ArtifactAction { - return { - icon: , - description: getLocaleText('en').t('artifact.sheet.actions.undo'), - onClick: ({ handleVersionChange }) => { - handleVersionChange('prev'); - }, - isDisabled: ({ currentVersionIndex }) => { - return currentVersionIndex === 0; - }, - }; -} diff --git a/src/features/documents/artifacts/sheet/components/sheet-content.tsx b/src/features/documents/artifacts/sheet/components/sheet-content.tsx deleted file mode 100644 index 7e7b193f..00000000 --- a/src/features/documents/artifacts/sheet/components/sheet-content.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { SpreadsheetEditor } from '@/features/documents/artifacts/sheet/components/sheet-editor'; -import type { ArtifactContent } from '@/features/documents/artifacts/types'; - -type Metadata = any; - -export function SheetContent(props: ArtifactContent) { - return ( - - ); -} diff --git a/src/features/documents/artifacts/sheet/components/sheet-editor.tsx b/src/features/documents/artifacts/sheet/components/sheet-editor.tsx deleted file mode 100644 index 7107b468..00000000 --- a/src/features/documents/artifacts/sheet/components/sheet-editor.tsx +++ /dev/null @@ -1,143 +0,0 @@ -'use client'; - -import { parse, unparse } from 'papaparse'; -import { memo, useEffect, useMemo, useState } from 'react'; -import { DataGrid, textEditor } from 'react-data-grid'; -import { useTheme } from '@/shared/components/theme-provider'; -import { cn } from '@/shared/utils'; - -import 'react-data-grid/lib/styles.css'; - -type SheetEditorProps = { - content: string; - saveContent: (content: string, isCurrentVersion: boolean) => void; - status: string; - isCurrentVersion: boolean; - currentVersionIndex: number; -}; - -const MIN_ROWS = 50; -const MIN_COLS = 26; - -const PureSpreadsheetEditor = ({ - content, - saveContent, - status, - isCurrentVersion, -}: SheetEditorProps) => { - const { resolvedTheme } = useTheme(); - - const parseData = useMemo(() => { - if (!content) return Array(MIN_ROWS).fill(Array(MIN_COLS).fill('')); - const result = parse(content, { skipEmptyLines: true }); - - const paddedData = result.data.map((row) => { - const paddedRow = [...row]; - while (paddedRow.length < MIN_COLS) { - paddedRow.push(''); - } - return paddedRow; - }); - - while (paddedData.length < MIN_ROWS) { - paddedData.push(Array(MIN_COLS).fill('')); - } - - return paddedData; - }, [content]); - - const columns = useMemo(() => { - const rowNumberColumn = { - key: 'rowNumber', - name: '', - frozen: true, - width: 50, - renderCell: ({ rowIdx }: { rowIdx: number }) => rowIdx + 1, - cellClass: 'border-t border-r dark:bg-zinc-950 dark:text-zinc-50', - headerCellClass: 'border-t border-r dark:bg-zinc-900 dark:text-zinc-50', - }; - - const dataColumns = Array.from({ length: MIN_COLS }, (_, i) => ({ - key: i.toString(), - name: String.fromCharCode(65 + i), - renderEditCell: textEditor, - width: 120, - cellClass: cn(`border-t dark:bg-zinc-950 dark:text-zinc-50`, { - 'border-l': i !== 0, - }), - headerCellClass: cn(`border-t dark:bg-zinc-900 dark:text-zinc-50`, { - 'border-l': i !== 0, - }), - })); - - return [rowNumberColumn, ...dataColumns]; - }, []); - - const initialRows = useMemo(() => { - return parseData.map((row, rowIndex) => { - const rowData: any = { - id: rowIndex, - rowNumber: rowIndex + 1, - }; - - columns.slice(1).forEach((col, colIndex) => { - rowData[col.key] = row[colIndex] || ''; - }); - - return rowData; - }); - }, [parseData, columns]); - - const [localRows, setLocalRows] = useState(initialRows); - - useEffect(() => { - setLocalRows(initialRows); - }, [initialRows]); - - const generateCsv = (data: any[][]) => { - return unparse(data); - }; - - const handleRowsChange = (newRows: any[]) => { - setLocalRows(newRows); - - const updatedData = newRows.map((row) => { - return columns.slice(1).map((col) => row[col.key] || ''); - }); - - const newCsvContent = generateCsv(updatedData); - saveContent(newCsvContent, true); - }; - - return ( - { - if (args.column.key !== 'rowNumber') { - args.selectCell(true); - } - }} - style={{ height: '100%' }} - defaultColumnOptions={{ - resizable: true, - sortable: true, - }} - /> - ); -}; - -function areEqual(prevProps: SheetEditorProps, nextProps: SheetEditorProps) { - return ( - prevProps.currentVersionIndex === nextProps.currentVersionIndex && - prevProps.isCurrentVersion === nextProps.isCurrentVersion && - !(prevProps.status === 'streaming' && nextProps.status === 'streaming') && - prevProps.content === nextProps.content && - prevProps.saveContent === nextProps.saveContent - ); -} - -export const SpreadsheetEditor = memo(PureSpreadsheetEditor, areEqual); diff --git a/src/features/documents/artifacts/sheet/components/sheet-preview.tsx b/src/features/documents/artifacts/sheet/components/sheet-preview.tsx deleted file mode 100644 index 8fbc9726..00000000 --- a/src/features/documents/artifacts/sheet/components/sheet-preview.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { SpreadsheetEditor } from '@/features/documents/artifacts/sheet/components/sheet-editor'; -import type { Document } from '@/features/documents/stores'; - -interface SheetPreviewProps { - document: Document; - editorStatus: 'streaming' | 'idle'; -} - -export function SheetPreview({ document, editorStatus }: SheetPreviewProps) { - const commonProps = { - content: document.content ?? '', - isCurrentVersion: true, - currentVersionIndex: 0, - status: editorStatus, - saveContent: () => {}, - suggestions: [], - }; - - return ( -
-
- -
-
- ); -} diff --git a/src/features/documents/artifacts/sheet/generator.ts b/src/features/documents/artifacts/sheet/generator.ts deleted file mode 100644 index 49cf767e..00000000 --- a/src/features/documents/artifacts/sheet/generator.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { streamObject } from 'ai'; -import { z } from 'zod'; -import { llmProvider } from '@/features/ai-provider/services'; - -export const sheetPrompt = ` -You are a spreadsheet creation assistant. Create a spreadsheet in csv format based on the given prompt. The spreadsheet should contain meaningful column headers and data. -`; - -export async function generateSheetContent( - title: string, - onDelta: (delta: string) => void, -): Promise { - let draftContent = ''; - - const { fullStream } = streamObject({ - model: llmProvider.artifact(), - system: sheetPrompt, - prompt: title, - schema: z.object({ - csv: z.string().describe('CSV data'), - }), - }); - - for await (const delta of fullStream) { - if (delta.type === 'object' && delta.object.csv) { - draftContent = delta.object.csv; - onDelta(delta.object.csv); - } - } - - return draftContent; -} diff --git a/src/features/documents/artifacts/sheet/index.tsx b/src/features/documents/artifacts/sheet/index.tsx deleted file mode 100644 index bbe290b7..00000000 --- a/src/features/documents/artifacts/sheet/index.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { Artifact } from '@/features/documents/artifacts/types'; -import { createCopyAction } from './actions/copy'; -import { createRedoAction } from './actions/redo'; -import { createUndoAction } from './actions/undo'; -import { SheetContent } from './components/sheet-content'; - -type Metadata = any; - -export const createSheetArtifact = () => { - return new Artifact<'sheet', Metadata>({ - kind: 'sheet', - description: 'Sheet artifact for displaying spreadsheets', - initialize: async () => {}, - content: SheetContent, - actions: [createUndoAction(), createRedoAction(), createCopyAction()], - }); -}; - -export const sheetArtifact = createSheetArtifact(); - -export { generateSheetContent } from './generator'; -export { updateSheetContent } from './updater'; diff --git a/src/features/documents/artifacts/sheet/updater.ts b/src/features/documents/artifacts/sheet/updater.ts deleted file mode 100644 index 9428aab8..00000000 --- a/src/features/documents/artifacts/sheet/updater.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { streamObject } from 'ai'; -import { z } from 'zod'; -import { llmProvider } from '@/features/ai-provider/services'; - -export const updateDocumentPrompt = (currentContent: string | null) => - `\ -Improve the following spreadsheet based on the given prompt. - -${currentContent} -`; - -export async function updateSheetContent( - currentContent: string, - description: string, - onDelta: (delta: string) => void, -): Promise { - let draftContent = ''; - - const { fullStream } = streamObject({ - model: llmProvider.artifact(), - system: updateDocumentPrompt(currentContent), - prompt: description, - schema: z.object({ - csv: z.string(), - }), - }); - - for await (const delta of fullStream) { - if (delta.type === 'object' && delta.object.csv) { - draftContent = delta.object.csv; - onDelta(delta.object.csv); - } - } - - return draftContent; -} diff --git a/src/features/documents/artifacts/text/actions/copy.tsx b/src/features/documents/artifacts/text/actions/copy.tsx deleted file mode 100644 index 7e472e85..00000000 --- a/src/features/documents/artifacts/text/actions/copy.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { CopyIcon } from 'lucide-react'; -import { toast } from '@/shared/components/toast'; -import { getLocaleText } from '@/shared/locales'; -import type { ArtifactAction } from '../../types'; - -export function createCopyAction(): ArtifactAction { - return { - icon: , - description: getLocaleText('en').t('artifact.text.actions.copy'), - onClick: ({ content }) => { - navigator.clipboard.writeText(content); - toast({ - description: getLocaleText('en').t('artifact.copied'), - type: 'success', - }); - }, - }; -} diff --git a/src/features/documents/artifacts/text/actions/redo.tsx b/src/features/documents/artifacts/text/actions/redo.tsx deleted file mode 100644 index bf85b85e..00000000 --- a/src/features/documents/artifacts/text/actions/redo.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { RedoIcon } from 'lucide-react'; -import { getLocaleText } from '@/shared/locales'; -import type { ArtifactAction } from '../../types'; - -export function createRedoAction(): ArtifactAction { - return { - icon: , - description: getLocaleText('en').t('artifact.text.actions.redo'), - onClick: ({ handleVersionChange }) => { - handleVersionChange('next'); - }, - isDisabled: ({ isCurrentVersion }) => { - return isCurrentVersion; - }, - }; -} diff --git a/src/features/documents/artifacts/text/actions/undo.tsx b/src/features/documents/artifacts/text/actions/undo.tsx deleted file mode 100644 index 34ee1194..00000000 --- a/src/features/documents/artifacts/text/actions/undo.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { UndoIcon } from 'lucide-react'; -import { getLocaleText } from '@/shared/locales'; -import type { ArtifactAction } from '../../types'; - -export function createUndoAction(): ArtifactAction { - return { - icon: , - description: getLocaleText('en').t('artifact.text.actions.undo'), - onClick: ({ handleVersionChange }) => { - handleVersionChange('prev'); - }, - isDisabled: ({ currentVersionIndex }) => { - return currentVersionIndex === 0; - }, - }; -} diff --git a/src/features/documents/artifacts/text/actions/version-change.tsx b/src/features/documents/artifacts/text/actions/version-change.tsx deleted file mode 100644 index 9774f375..00000000 --- a/src/features/documents/artifacts/text/actions/version-change.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { FileDiff } from 'lucide-react'; -import { getLocaleText } from '@/shared/locales'; -import type { ArtifactAction } from '../../types'; - -export function createVersionChangeAction(): ArtifactAction { - return { - icon: , - description: getLocaleText('en').t('artifact.text.actions.versionChange'), - onClick: ({ handleVersionChange }) => { - handleVersionChange('toggle'); - }, - isDisabled: ({ currentVersionIndex }) => { - return currentVersionIndex === 0; - }, - }; -} diff --git a/src/features/documents/artifacts/text/components/diffview.tsx b/src/features/documents/artifacts/text/components/diffview.tsx deleted file mode 100644 index a7fb8393..00000000 --- a/src/features/documents/artifacts/text/components/diffview.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import OrderedMap from 'orderedmap'; -import { - DOMParser, - type MarkSpec, - type Node as ProsemirrorNode, - Schema, -} from 'prosemirror-model'; -import { schema } from 'prosemirror-schema-basic'; -import { addListNodes } from 'prosemirror-schema-list'; -import { EditorState } from 'prosemirror-state'; -import { EditorView } from 'prosemirror-view'; -import { useEffect, useRef } from 'react'; -import { renderToString } from 'react-dom/server'; -import { DiffType, diffEditor } from './editor/diff'; -import { Markdown } from './editor/markdown'; - -const diffSchema = new Schema({ - nodes: addListNodes(schema.spec.nodes, 'paragraph block*', 'block'), - marks: OrderedMap.from({ - ...schema.spec.marks.toObject(), - diffMark: { - attrs: { type: { default: '' } }, - toDOM(mark) { - let className = ''; - - switch (mark.attrs.type) { - case DiffType.Inserted: - className = - 'bg-green-100 text-green-700 dark:bg-green-500/70 dark:text-green-300'; - break; - case DiffType.Deleted: - className = - 'bg-red-100 line-through text-red-600 dark:bg-red-500/70 dark:text-red-300'; - break; - default: - className = ''; - } - return ['span', { class: className }, 0]; - }, - } as MarkSpec, - }), -}); - -function computeDiff(oldDoc: ProsemirrorNode, newDoc: ProsemirrorNode) { - return diffEditor(diffSchema, oldDoc.toJSON(), newDoc.toJSON()); -} - -type DiffEditorProps = { - oldContent: string; - newContent: string; -}; - -export const DiffView = ({ oldContent, newContent }: DiffEditorProps) => { - const editorRef = useRef(null); - const viewRef = useRef(null); - - useEffect(() => { - if (editorRef.current && !viewRef.current) { - const parser = DOMParser.fromSchema(diffSchema); - - const oldHtmlContent = renderToString({oldContent}); - const newHtmlContent = renderToString({newContent}); - - const oldContainer = document.createElement('div'); - oldContainer.innerHTML = oldHtmlContent; - - const newContainer = document.createElement('div'); - newContainer.innerHTML = newHtmlContent; - - const oldDoc = parser.parse(oldContainer); - const newDoc = parser.parse(newContainer); - - const diffedDoc = computeDiff(oldDoc, newDoc); - - const state = EditorState.create({ - doc: diffedDoc, - plugins: [], - }); - - viewRef.current = new EditorView(editorRef.current, { - state, - editable: () => false, - }); - } - - return () => { - if (viewRef.current) { - viewRef.current.destroy(); - viewRef.current = null; - } - }; - }, [oldContent, newContent]); - - return
; -}; diff --git a/src/features/documents/artifacts/text/components/editor/code-block.tsx b/src/features/documents/artifacts/text/components/editor/code-block.tsx deleted file mode 100644 index c0780e58..00000000 --- a/src/features/documents/artifacts/text/components/editor/code-block.tsx +++ /dev/null @@ -1,38 +0,0 @@ -'use client'; - -interface CodeBlockProps { - node: any; - inline: boolean; - className: string; - children: any; -} - -export function CodeBlock({ - node, - inline, - className, - children, - ...props -}: CodeBlockProps) { - if (!inline) { - return ( -
-
-          {children}
-        
-
- ); - } else { - return ( - - {children} - - ); - } -} diff --git a/src/features/documents/artifacts/text/components/editor/config.ts b/src/features/documents/artifacts/text/components/editor/config.ts deleted file mode 100644 index c171cac3..00000000 --- a/src/features/documents/artifacts/text/components/editor/config.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { textblockTypeInputRule } from 'prosemirror-inputrules'; -import { Schema } from 'prosemirror-model'; -import { schema } from 'prosemirror-schema-basic'; -import { addListNodes } from 'prosemirror-schema-list'; -import type { Transaction } from 'prosemirror-state'; -import type { EditorView } from 'prosemirror-view'; -import type { MutableRefObject } from 'react'; - -import { buildContentFromDocument } from './functions'; - -export const documentSchema = new Schema({ - nodes: addListNodes(schema.spec.nodes, 'paragraph block*', 'block'), - marks: schema.spec.marks, -}); - -export function headingRule(level: number) { - return textblockTypeInputRule( - new RegExp(`^(#{1,${level}})\\s$`), - documentSchema.nodes.heading, - () => ({ level }), - ); -} - -export const handleTransaction = ({ - transaction, - editorRef, - onSaveContent, -}: { - transaction: Transaction; - editorRef: MutableRefObject; - onSaveContent: (updatedContent: string, debounce: boolean) => void; -}) => { - if (!editorRef || !editorRef.current) return; - - const newState = editorRef.current.state.apply(transaction); - editorRef.current.updateState(newState); - - if (transaction.docChanged && !transaction.getMeta('no-save')) { - const updatedContent = buildContentFromDocument(newState.doc); - - if (transaction.getMeta('no-debounce')) { - onSaveContent(updatedContent, false); - } else { - onSaveContent(updatedContent, true); - } - } -}; diff --git a/src/features/documents/artifacts/text/components/editor/diff.js b/src/features/documents/artifacts/text/components/editor/diff.js deleted file mode 100644 index dd9bc920..00000000 --- a/src/features/documents/artifacts/text/components/editor/diff.js +++ /dev/null @@ -1,475 +0,0 @@ -// Modified from https://github.com/hamflx/prosemirror-diff/blob/master/src/diff.js - -import { diff_match_patch } from 'diff-match-patch'; -import { Fragment, Node } from 'prosemirror-model'; - -export const DiffType = { - Unchanged: 0, - Deleted: -1, - Inserted: 1, -}; - -export const patchDocumentNode = (schema, oldNode, newNode) => { - assertNodeTypeEqual(oldNode, newNode); - - const finalLeftChildren = []; - const finalRightChildren = []; - - const oldChildren = normalizeNodeContent(oldNode); - const newChildren = normalizeNodeContent(newNode); - const oldChildLen = oldChildren.length; - const newChildLen = newChildren.length; - const minChildLen = Math.min(oldChildLen, newChildLen); - - let left = 0; - let right = 0; - - for (; left < minChildLen; left++) { - const oldChild = oldChildren[left]; - const newChild = newChildren[left]; - if (!isNodeEqual(oldChild, newChild)) { - break; - } - finalLeftChildren.push(...ensureArray(oldChild)); - } - - for (; right + left + 1 < minChildLen; right++) { - const oldChild = oldChildren[oldChildLen - right - 1]; - const newChild = newChildren[newChildLen - right - 1]; - if (!isNodeEqual(oldChild, newChild)) { - break; - } - finalRightChildren.unshift(...ensureArray(oldChild)); - } - - const diffOldChildren = oldChildren.slice(left, oldChildLen - right); - const diffNewChildren = newChildren.slice(left, newChildLen - right); - - if (diffOldChildren.length && diffNewChildren.length) { - const matchedNodes = matchNodes( - schema, - diffOldChildren, - diffNewChildren, - ).sort((a, b) => b.count - a.count); - const bestMatch = matchedNodes[0]; - if (bestMatch) { - const { oldStartIndex, newStartIndex, oldEndIndex, newEndIndex } = - bestMatch; - const oldBeforeMatchChildren = diffOldChildren.slice(0, oldStartIndex); - const newBeforeMatchChildren = diffNewChildren.slice(0, newStartIndex); - - finalLeftChildren.push( - ...patchRemainNodes( - schema, - oldBeforeMatchChildren, - newBeforeMatchChildren, - ), - ); - finalLeftChildren.push( - ...diffOldChildren.slice(oldStartIndex, oldEndIndex), - ); - - const oldAfterMatchChildren = diffOldChildren.slice(oldEndIndex); - const newAfterMatchChildren = diffNewChildren.slice(newEndIndex); - - finalRightChildren.unshift( - ...patchRemainNodes( - schema, - oldAfterMatchChildren, - newAfterMatchChildren, - ), - ); - } else { - finalLeftChildren.push( - ...patchRemainNodes(schema, diffOldChildren, diffNewChildren), - ); - } - } else { - finalLeftChildren.push( - ...patchRemainNodes(schema, diffOldChildren, diffNewChildren), - ); - } - - return createNewNode(oldNode, [...finalLeftChildren, ...finalRightChildren]); -}; - -const matchNodes = (schema, oldChildren, newChildren) => { - const matches = []; - for ( - let oldStartIndex = 0; - oldStartIndex < oldChildren.length; - oldStartIndex++ - ) { - const oldStartNode = oldChildren[oldStartIndex]; - const newStartIndex = findMatchNode(newChildren, oldStartNode); - - if (newStartIndex !== -1) { - let oldEndIndex = oldStartIndex + 1; - let newEndIndex = newStartIndex + 1; - for ( - ; - oldEndIndex < oldChildren.length && newEndIndex < newChildren.length; - oldEndIndex++, newEndIndex++ - ) { - const oldEndNode = oldChildren[oldEndIndex]; - if (!isNodeEqual(newChildren[newEndIndex], oldEndNode)) { - break; - } - } - matches.push({ - oldStartIndex, - newStartIndex, - oldEndIndex, - newEndIndex, - count: newEndIndex - newStartIndex, - }); - } - } - return matches; -}; - -const findMatchNode = (children, node, startIndex = 0) => { - for (let i = startIndex; i < children.length; i++) { - if (isNodeEqual(children[i], node)) { - return i; - } - } - return -1; -}; - -const patchRemainNodes = (schema, oldChildren, newChildren) => { - const finalLeftChildren = []; - const finalRightChildren = []; - const oldChildLen = oldChildren.length; - const newChildLen = newChildren.length; - let left = 0; - let right = 0; - while (oldChildLen - left - right > 0 && newChildLen - left - right > 0) { - const leftOldNode = oldChildren[left]; - const leftNewNode = newChildren[left]; - const rightOldNode = oldChildren[oldChildLen - right - 1]; - const rightNewNode = newChildren[newChildLen - right - 1]; - let updateLeft = - !isTextNode(leftOldNode) && matchNodeType(leftOldNode, leftNewNode); - let updateRight = - !isTextNode(rightOldNode) && matchNodeType(rightOldNode, rightNewNode); - if (Array.isArray(leftOldNode) && Array.isArray(leftNewNode)) { - finalLeftChildren.push( - ...patchTextNodes(schema, leftOldNode, leftNewNode), - ); - left += 1; - continue; - } - - if (updateLeft && updateRight) { - const equalityLeft = computeChildEqualityFactor(leftOldNode, leftNewNode); - const equalityRight = computeChildEqualityFactor( - rightOldNode, - rightNewNode, - ); - if (equalityLeft < equalityRight) { - updateLeft = false; - } else { - updateRight = false; - } - } - if (updateLeft) { - finalLeftChildren.push( - patchDocumentNode(schema, leftOldNode, leftNewNode), - ); - left += 1; - } else if (updateRight) { - finalRightChildren.unshift( - patchDocumentNode(schema, rightOldNode, rightNewNode), - ); - right += 1; - } else { - // Delete and insert - finalLeftChildren.push( - createDiffNode(schema, leftOldNode, DiffType.Deleted), - ); - finalLeftChildren.push( - createDiffNode(schema, leftNewNode, DiffType.Inserted), - ); - left += 1; - } - } - - const deleteNodeLen = oldChildLen - left - right; - const insertNodeLen = newChildLen - left - right; - if (deleteNodeLen) { - finalLeftChildren.push( - ...oldChildren - .slice(left, left + deleteNodeLen) - .flat() - .map((node) => createDiffNode(schema, node, DiffType.Deleted)), - ); - } - - if (insertNodeLen) { - finalRightChildren.unshift( - ...newChildren - .slice(left, left + insertNodeLen) - .flat() - .map((node) => createDiffNode(schema, node, DiffType.Inserted)), - ); - } - - return [...finalLeftChildren, ...finalRightChildren]; -}; - -// Updated function to perform sentence-level diffs -export const patchTextNodes = (schema, oldNode, newNode) => { - const dmp = new diff_match_patch(); - - // Concatenate the text from the text nodes - const oldText = oldNode.map((n) => getNodeText(n)).join(''); - const newText = newNode.map((n) => getNodeText(n)).join(''); - - // Tokenize the text into sentences - const oldSentences = tokenizeSentences(oldText); - const newSentences = tokenizeSentences(newText); - - // Map sentences to unique characters - const { chars1, chars2, lineArray } = sentencesToChars( - oldSentences, - newSentences, - ); - - // Perform the diff - let diffs = dmp.diff_main(chars1, chars2, false); - - // Convert back to sentences - diffs = diffs.map(([type, text]) => { - const sentences = text - .split('') - .map((char) => lineArray[char.charCodeAt(0)]); - return [type, sentences]; - }); - - // Map diffs to nodes - const res = diffs.flatMap(([type, sentences]) => { - return sentences.map((sentence) => { - const node = createTextNode( - schema, - sentence, - type !== DiffType.Unchanged ? [createDiffMark(schema, type)] : [], - ); - return node; - }); - }); - - return res; -}; - -// Function to tokenize text into sentences -const tokenizeSentences = (text) => { - return text.match(/[^.!?]+[.!?]*\s*/g) || []; -}; - -// Function to map sentences to unique characters -const sentencesToChars = (oldSentences, newSentences) => { - const lineArray = []; - const lineHash = {}; - let lineStart = 0; - - const chars1 = oldSentences - .map((sentence) => { - const line = sentence; - if (line in lineHash) { - return String.fromCharCode(lineHash[line]); - } - lineHash[line] = lineStart; - lineArray[lineStart] = line; - lineStart++; - return String.fromCharCode(lineHash[line]); - }) - .join(''); - - const chars2 = newSentences - .map((sentence) => { - const line = sentence; - if (line in lineHash) { - return String.fromCharCode(lineHash[line]); - } - lineHash[line] = lineStart; - lineArray[lineStart] = line; - lineStart++; - return String.fromCharCode(lineHash[line]); - }) - .join(''); - - return { chars1, chars2, lineArray }; -}; - -export const computeChildEqualityFactor = (node1, node2) => { - return 0; -}; - -export const assertNodeTypeEqual = (node1, node2) => { - if (getNodeProperty(node1, 'type') !== getNodeProperty(node2, 'type')) { - throw new Error(`node type not equal: ${node1.type} !== ${node2.type}`); - } -}; - -export const ensureArray = (value) => { - return Array.isArray(value) ? value : [value]; -}; - -export const isNodeEqual = (node1, node2) => { - const isNode1Array = Array.isArray(node1); - const isNode2Array = Array.isArray(node2); - if (isNode1Array !== isNode2Array) { - return false; - } - if (isNode1Array) { - return ( - node1.length === node2.length && - node1.every((node, index) => isNodeEqual(node, node2[index])) - ); - } - - const type1 = getNodeProperty(node1, 'type'); - const type2 = getNodeProperty(node2, 'type'); - if (type1 !== type2) { - return false; - } - if (isTextNode(node1)) { - const text1 = getNodeProperty(node1, 'text'); - const text2 = getNodeProperty(node2, 'text'); - if (text1 !== text2) { - return false; - } - } - const attrs1 = getNodeAttributes(node1); - const attrs2 = getNodeAttributes(node2); - const attrs = [...new Set([...Object.keys(attrs1), ...Object.keys(attrs2)])]; - for (const attr of attrs) { - if (attrs1[attr] !== attrs2[attr]) { - return false; - } - } - const marks1 = getNodeMarks(node1); - const marks2 = getNodeMarks(node2); - if (marks1.length !== marks2.length) { - return false; - } - for (let i = 0; i < marks1.length; i++) { - if (!isNodeEqual(marks1[i], marks2[i])) { - return false; - } - } - const children1 = getNodeChildren(node1); - const children2 = getNodeChildren(node2); - if (children1.length !== children2.length) { - return false; - } - for (let i = 0; i < children1.length; i++) { - if (!isNodeEqual(children1[i], children2[i])) { - return false; - } - } - return true; -}; - -export const normalizeNodeContent = (node) => { - const content = getNodeChildren(node) ?? []; - const res = []; - for (let i = 0; i < content.length; i++) { - const child = content[i]; - if (isTextNode(child)) { - const textNodes = []; - for ( - let textNode = content[i]; - i < content.length && isTextNode(textNode); - textNode = content[++i] - ) { - textNodes.push(textNode); - } - i--; - res.push(textNodes); - } else { - res.push(child); - } - } - return res; -}; - -export const getNodeProperty = (node, property) => { - if (property === 'type') { - return node.type?.name; - } - return node[property]; -}; - -export const getNodeAttribute = (node, attribute) => - node.attrs ? node.attrs[attribute] : undefined; - -export const getNodeAttributes = (node) => (node.attrs ? node.attrs : {}); - -export const getNodeMarks = (node) => node.marks ?? []; - -export const getNodeChildren = (node) => node.content?.content ?? []; - -export const getNodeText = (node) => node.text; - -export const isTextNode = (node) => node.type?.name === 'text'; - -export const matchNodeType = (node1, node2) => - node1.type?.name === node2.type?.name || - (Array.isArray(node1) && Array.isArray(node2)); - -export const createNewNode = (oldNode, children) => { - if (!oldNode.type) { - throw new Error('oldNode.type is undefined'); - } - return new Node( - oldNode.type, - oldNode.attrs, - Fragment.fromArray(children), - oldNode.marks, - ); -}; - -export const createDiffNode = (schema, node, type) => { - return mapDocumentNode(node, (node) => { - if (isTextNode(node)) { - return createTextNode(schema, getNodeText(node), [ - ...(node.marks || []), - createDiffMark(schema, type), - ]); - } - return node; - }); -}; - -function mapDocumentNode(node, mapper) { - const copy = node.copy( - Fragment.from( - node.content.content - .map((node) => mapDocumentNode(node, mapper)) - .filter((n) => n), - ), - ); - return mapper(copy) || copy; -} - -export const createDiffMark = (schema, type) => { - if (type === DiffType.Inserted) { - return schema.mark('diffMark', { type }); - } - if (type === DiffType.Deleted) { - return schema.mark('diffMark', { type }); - } - throw new Error('type is not valid'); -}; - -export const createTextNode = (schema, content, marks = []) => { - return schema.text(content, marks); -}; - -export const diffEditor = (schema, oldDoc, newDoc) => { - const oldNode = Node.fromJSON(schema, oldDoc); - const newNode = Node.fromJSON(schema, newDoc); - return patchDocumentNode(schema, oldNode, newNode); -}; diff --git a/src/features/documents/artifacts/text/components/editor/functions.tsx b/src/features/documents/artifacts/text/components/editor/functions.tsx deleted file mode 100644 index 12582e2c..00000000 --- a/src/features/documents/artifacts/text/components/editor/functions.tsx +++ /dev/null @@ -1,19 +0,0 @@ -'use client'; - -import { defaultMarkdownSerializer } from 'prosemirror-markdown'; -import { DOMParser, type Node } from 'prosemirror-model'; -import { renderToString } from 'react-dom/server'; -import { documentSchema } from './config'; -import { Markdown } from './markdown'; - -export const buildDocumentFromContent = (content: string) => { - const parser = DOMParser.fromSchema(documentSchema); - const stringFromMarkdown = renderToString({content}); - const tempContainer = document.createElement('div'); - tempContainer.innerHTML = stringFromMarkdown; - return parser.parse(tempContainer); -}; - -export const buildContentFromDocument = (document: Node) => { - return defaultMarkdownSerializer.serialize(document); -}; diff --git a/src/features/documents/artifacts/text/components/editor/markdown.css b/src/features/documents/artifacts/text/components/editor/markdown.css deleted file mode 100644 index 98649e6d..00000000 --- a/src/features/documents/artifacts/text/components/editor/markdown.css +++ /dev/null @@ -1,13 +0,0 @@ -.markdown-preview-content ul { - list-style-type: disc !important; - padding-left: 1.5rem !important; - } - -.markdown-preview-content ol { -list-style-type: decimal !important; -padding-left: 1.5rem !important; -} - -.markdown-preview-content pre code { - min-width: 100% !important; -} \ No newline at end of file diff --git a/src/features/documents/artifacts/text/components/editor/markdown.tsx b/src/features/documents/artifacts/text/components/editor/markdown.tsx deleted file mode 100644 index 28a52c25..00000000 --- a/src/features/documents/artifacts/text/components/editor/markdown.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import MarkdownPreview from '@uiw/react-markdown-preview'; -import { memo } from 'react'; -import { useTheme } from '@/shared/components/theme-provider'; -import './markdown.css'; - -const NonMemoizedMarkdown = ({ children }: { children: string }) => { - const { resolvedTheme } = useTheme(); - return ( - - ); -}; - -export const Markdown = memo( - NonMemoizedMarkdown, - (prevProps, nextProps) => prevProps.children === nextProps.children, -); diff --git a/src/features/documents/artifacts/text/components/editor/react-renderer.tsx b/src/features/documents/artifacts/text/components/editor/react-renderer.tsx deleted file mode 100644 index 5a203a78..00000000 --- a/src/features/documents/artifacts/text/components/editor/react-renderer.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { createRoot } from 'react-dom/client'; - -export const ReactRenderer = { - render(component: React.ReactElement, dom: HTMLElement) { - const root = createRoot(dom); - root.render(component); - - return { - destroy: () => root.unmount(), - }; - }, -}; diff --git a/src/features/documents/artifacts/text/components/text-content.tsx b/src/features/documents/artifacts/text/components/text-content.tsx deleted file mode 100644 index cd70bcb5..00000000 --- a/src/features/documents/artifacts/text/components/text-content.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { DiffView } from '@/features/documents/artifacts/text/components/diffview'; -import { Editor } from '@/features/documents/artifacts/text/components/text-editor'; -import type { ArtifactContent } from '@/features/documents/artifacts/types'; - -const Skeleton = () => { - return ( -
-
-
-
-
-
-
-
-
- ); -}; - -export function TextContent(props: ArtifactContent) { - const { - mode, - status, - content, - isCurrentVersion, - currentVersionIndex, - onSaveContent, - getDocumentContentById, - isLoading, - } = props; - - if (isLoading) { - return ; - } - - if (mode === 'diff') { - const oldContent = getDocumentContentById(currentVersionIndex - 1); - const newContent = getDocumentContentById(currentVersionIndex); - return ; - } - - return ( -
- -
- ); -} diff --git a/src/features/documents/artifacts/text/components/text-editor.tsx b/src/features/documents/artifacts/text/components/text-editor.tsx deleted file mode 100644 index dfed2ed5..00000000 --- a/src/features/documents/artifacts/text/components/text-editor.tsx +++ /dev/null @@ -1,129 +0,0 @@ -'use client'; - -import { exampleSetup } from 'prosemirror-example-setup'; -import { inputRules } from 'prosemirror-inputrules'; -import { EditorState } from 'prosemirror-state'; -import { EditorView } from 'prosemirror-view'; -import { memo, useEffect, useRef } from 'react'; - -import { - documentSchema, - handleTransaction, - headingRule, -} from './editor/config'; -import { - buildContentFromDocument, - buildDocumentFromContent, -} from './editor/functions'; - -type EditorProps = { - content: string; - onSaveContent: (updatedContent: string, debounce: boolean) => void; - status: 'streaming' | 'idle'; - isCurrentVersion: boolean; - currentVersionIndex: number; -}; - -function PureEditor({ content, onSaveContent, status }: EditorProps) { - const containerRef = useRef(null); - const editorRef = useRef(null); - - useEffect(() => { - if (containerRef.current && !editorRef.current) { - const state = EditorState.create({ - doc: buildDocumentFromContent(content), - plugins: [ - ...exampleSetup({ schema: documentSchema, menuBar: false }), - inputRules({ - rules: [ - headingRule(1), - headingRule(2), - headingRule(3), - headingRule(4), - headingRule(5), - headingRule(6), - ], - }), - ], - }); - - editorRef.current = new EditorView(containerRef.current, { - state, - }); - } - - return () => { - if (editorRef.current) { - editorRef.current.destroy(); - editorRef.current = null; - } - }; - // NOTE: we only want to run this effect once - // eslint-disable-next-line - }, []); - - useEffect(() => { - if (editorRef.current) { - editorRef.current.setProps({ - dispatchTransaction: (transaction) => { - handleTransaction({ - transaction, - editorRef, - onSaveContent, - }); - }, - }); - } - }, [onSaveContent]); - - useEffect(() => { - if (editorRef.current && content) { - const currentContent = buildContentFromDocument( - editorRef.current.state.doc, - ); - - if (status === 'streaming') { - const newDocument = buildDocumentFromContent(content); - - const transaction = editorRef.current.state.tr.replaceWith( - 0, - editorRef.current.state.doc.content.size, - newDocument.content, - ); - - transaction.setMeta('no-save', true); - editorRef.current.dispatch(transaction); - return; - } - - if (currentContent !== content) { - const newDocument = buildDocumentFromContent(content); - - const transaction = editorRef.current.state.tr.replaceWith( - 0, - editorRef.current.state.doc.content.size, - newDocument.content, - ); - - transaction.setMeta('no-save', true); - editorRef.current.dispatch(transaction); - } - } - }, [content, status]); - - return ( -
- ); -} - -function areEqual(prevProps: EditorProps, nextProps: EditorProps) { - return ( - prevProps.currentVersionIndex === nextProps.currentVersionIndex && - prevProps.isCurrentVersion === nextProps.isCurrentVersion && - !(prevProps.status === 'streaming' && nextProps.status === 'streaming') && - prevProps.content === nextProps.content && - prevProps.onSaveContent === nextProps.onSaveContent - ); -} - -export const Editor = memo(PureEditor, areEqual); diff --git a/src/features/documents/artifacts/text/components/text-preview.tsx b/src/features/documents/artifacts/text/components/text-preview.tsx deleted file mode 100644 index 8741d75a..00000000 --- a/src/features/documents/artifacts/text/components/text-preview.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Editor } from '@/features/documents/artifacts/text/components/text-editor'; -import type { Document } from '@/features/documents/stores'; - -interface TextPreviewProps { - document: Document; - editorStatus: 'streaming' | 'idle'; -} - -export function TextPreview({ document, editorStatus }: TextPreviewProps) { - const commonProps = { - content: document.content ?? '', - isCurrentVersion: true, - currentVersionIndex: 0, - status: editorStatus, - saveContent: () => {}, - suggestions: [], - }; - - return {}} />; -} diff --git a/src/features/documents/artifacts/text/generator.ts b/src/features/documents/artifacts/text/generator.ts deleted file mode 100644 index d40e28c7..00000000 --- a/src/features/documents/artifacts/text/generator.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { smoothStream, streamText } from 'ai'; -import { llmProvider } from '@/features/ai-provider/services'; - -export async function generateTextContent( - title: string, - onDelta: (delta: string) => void, -): Promise { - let draftContent = ''; - - const { fullStream } = streamText({ - model: llmProvider.artifact(), - system: - 'Write about the given topic. Markdown is supported. Use headings wherever appropriate.', - experimental_transform: smoothStream({ chunking: 'word' }), - prompt: title, - }); - - for await (const delta of fullStream) { - if (delta.type === 'text-delta') { - const { textDelta } = delta; - draftContent += textDelta; - onDelta(textDelta); - } - } - - return draftContent; -} diff --git a/src/features/documents/artifacts/text/index.tsx b/src/features/documents/artifacts/text/index.tsx deleted file mode 100644 index d47beec3..00000000 --- a/src/features/documents/artifacts/text/index.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { Artifact } from '@/features/documents/artifacts/types'; -import { createCopyAction } from './actions/copy'; -import { createRedoAction } from './actions/redo'; -import { createUndoAction } from './actions/undo'; -import { createVersionChangeAction } from './actions/version-change'; -import { TextContent } from './components/text-content'; - -export const createTextArtifact = () => { - return new Artifact<'text'>({ - kind: 'text', - description: 'Text artifact for displaying and editing text', - initialize: async ({ documentId, setMetadata }) => {}, - content: TextContent, - actions: [ - createVersionChangeAction(), - createUndoAction(), - createRedoAction(), - createCopyAction(), - ], - }); -}; - -export const textArtifact = createTextArtifact(); - -export { generateTextContent } from './generator'; -export { updateTextContent } from './updater'; diff --git a/src/features/documents/artifacts/text/updater.ts b/src/features/documents/artifacts/text/updater.ts deleted file mode 100644 index c24b219c..00000000 --- a/src/features/documents/artifacts/text/updater.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { smoothStream, streamText } from 'ai'; -import { llmProvider } from '@/features/ai-provider/services'; - -export const updateDocumentPrompt = (currentContent: string | null) => - `\ -Improve the following contents of the document based on the given prompt. - -${currentContent} -`; - -export async function updateTextContent( - currentContent: string, - description: string, - onDelta: (delta: string) => void, -): Promise { - let draftContent = ''; - - const { fullStream } = streamText({ - model: llmProvider.artifact(), - system: updateDocumentPrompt(currentContent), - experimental_transform: smoothStream({ chunking: 'word' }), - prompt: description, - experimental_providerMetadata: { - openai: { - prediction: { - type: 'content', - content: currentContent, - }, - }, - }, - }); - - for await (const delta of fullStream) { - if (delta.type === 'text-delta') { - const { textDelta } = delta; - draftContent += textDelta; - onDelta(textDelta); - } - } - - return draftContent; -} diff --git a/src/features/documents/artifacts/types.tsx b/src/features/documents/artifacts/types.tsx deleted file mode 100644 index ed285796..00000000 --- a/src/features/documents/artifacts/types.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import type { ComponentType, Dispatch, ReactNode, SetStateAction } from 'react'; - -export type DataStreamDelta = { - type: - | 'text-delta' - | 'code-delta' - | 'sheet-delta' - | 'image-delta' - | 'title' - | 'id' - | 'clear' - | 'finish' - | 'kind'; - content: string; -}; - -export type ArtifactActionContext = { - content: string; - handleVersionChange: (type: 'next' | 'prev' | 'toggle' | 'latest') => void; - currentVersionIndex: number; - isCurrentVersion: boolean; - mode: 'edit' | 'diff'; - metadata: M; - setMetadata: Dispatch>; -}; - -export type ArtifactAction = { - icon: ReactNode; - label?: string; - description: string; - onClick: (context: ArtifactActionContext) => Promise | void; - isDisabled?: (context: ArtifactActionContext) => boolean; -}; - -export interface ArtifactContent { - title: string; - content: string; - mode: 'edit' | 'diff'; - isCurrentVersion: boolean; - currentVersionIndex: number; - status: 'streaming' | 'idle'; - onSaveContent: (updatedContent: string, debounce: boolean) => void; - isInline: boolean; - getDocumentContentById: (index: number) => string; - isLoading: boolean; - metadata: M; - setMetadata: Dispatch>; -} - -interface InitializeParameters { - documentId: string; - setMetadata: Dispatch>; -} - -type ArtifactConfig = { - kind: T; - description: string; - content: ComponentType>; - actions: Array>; - initialize?: (parameters: InitializeParameters) => void; -}; - -export class Artifact { - readonly kind: T; - readonly description: string; - readonly content: ComponentType>; - readonly actions: Array>; - readonly initialize?: (parameters: InitializeParameters) => void; - - constructor(config: ArtifactConfig) { - this.kind = config.kind; - this.description = config.description; - this.content = config.content; - this.actions = config.actions || []; - this.initialize = config.initialize || (async () => ({})); - } -} diff --git a/src/features/documents/components/artifact-actions.tsx b/src/features/documents/components/artifact-actions.tsx deleted file mode 100644 index b1a3f91c..00000000 --- a/src/features/documents/components/artifact-actions.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { type Dispatch, memo, type SetStateAction, useState } from 'react'; -import { artifactDefinitions } from '@/features/documents/artifacts'; -import type { ArtifactActionContext } from '@/features/documents/artifacts/types'; -import type { CurrentDocumentProps } from '@/features/documents/stores'; -import { toast } from '@/shared/components/toast'; -import { Button } from '@/shared/components/ui/button'; -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from '@/shared/components/ui/tooltip'; -import { cn } from '@/shared/utils'; - -interface ArtifactActionsProps { - artifact: CurrentDocumentProps; - handleVersionChange: (type: 'next' | 'prev' | 'toggle' | 'latest') => void; - currentVersionIndex: number; - isCurrentVersion: boolean; - mode: 'edit' | 'diff'; - metadata: any; - setMetadata: Dispatch>; -} - -function PureArtifactActions({ - artifact, - handleVersionChange, - currentVersionIndex, - isCurrentVersion, - mode, - metadata, - setMetadata, -}: ArtifactActionsProps) { - const [isLoading, setIsLoading] = useState(false); - - const artifactDefinition = artifactDefinitions.find( - (definition) => definition.kind === artifact.kind, - ); - - if (!artifactDefinition) { - throw new Error('Artifact definition not found!'); - } - - const actionContext: ArtifactActionContext = { - content: artifact.content, - handleVersionChange, - currentVersionIndex, - isCurrentVersion, - mode, - metadata, - setMetadata, - }; - - return ( -
- {artifactDefinition.actions.map((action) => ( - - - - - {action.description} - - ))} -
- ); -} - -export const ArtifactActions = memo( - PureArtifactActions, - (prevProps, nextProps) => { - if (prevProps.artifact.status !== nextProps.artifact.status) return false; - if (prevProps.currentVersionIndex !== nextProps.currentVersionIndex) - return false; - if (prevProps.isCurrentVersion !== nextProps.isCurrentVersion) return false; - if (prevProps.artifact.content !== nextProps.artifact.content) return false; - - return true; - }, -); diff --git a/src/features/documents/components/artifact-close-button.tsx b/src/features/documents/components/artifact-close-button.tsx deleted file mode 100644 index 616ad400..00000000 --- a/src/features/documents/components/artifact-close-button.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { X } from 'lucide-react'; -import { memo } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { useCurrentDocument } from '@/features/documents/hooks/use-document-current'; -import { Button } from '@/shared/components/ui/button'; - -function PureArtifactCloseButton({ chatId }: { chatId: string }) { - const { closeCurrentDocument } = useCurrentDocument(); - const navigate = useNavigate(); - - return ( - - ); -} - -export const ArtifactCloseButton = memo(PureArtifactCloseButton, () => true); diff --git a/src/features/documents/components/artifact-messages-header.tsx b/src/features/documents/components/artifact-messages-header.tsx deleted file mode 100644 index 66c3a114..00000000 --- a/src/features/documents/components/artifact-messages-header.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { PlusIcon } from 'lucide-react'; -import { useNavigate } from 'react-router-dom'; -import { useChatSessions } from '@/features/ai-chat/hooks/use-chat-sessions'; -import { Button } from '@/shared/components/ui/button'; -import { generateUUID } from '@/shared/utils'; - -export function ArtifactMessagesHeader({ chatId }: { chatId: string }) { - const navigate = useNavigate(); - - const { getSession } = useChatSessions(); - - const handleNewChat = () => { - const chatId = generateUUID(); - navigate(`/artifact?cid=${chatId}`); - }; - - return ( -
-
- {getSession(chatId)?.title || 'New Chat'} -
- -
- ); -} diff --git a/src/features/documents/components/artifact-viewer.tsx b/src/features/documents/components/artifact-viewer.tsx deleted file mode 100644 index 94537d1b..00000000 --- a/src/features/documents/components/artifact-viewer.tsx +++ /dev/null @@ -1,253 +0,0 @@ -import type { UseChatHelpers } from '@ai-sdk/react'; -import { formatDistance } from 'date-fns'; -import { useCallback, useEffect, useState } from 'react'; -import { useDebounceCallback } from 'usehooks-ts'; -import { artifactDefinitions } from '@/features/documents/artifacts'; -import { useCurrentDocument } from '@/features/documents/hooks/use-document-current'; -import { useDocuments } from '@/features/documents/hooks/use-documents'; -import type { Document } from '@/features/documents/stores'; -import { ArtifactActions } from './artifact-actions'; -import { ArtifactCloseButton } from './artifact-close-button'; -import { VersionFooter } from './version-footer'; - -interface ArtifactViewerProps { - chatId: string; - status: UseChatHelpers['status']; -} - -export function ArtifactViewer({ chatId, status }: ArtifactViewerProps) { - const { - currentDocument: artifact, - setCurrentDocument, - metadata, - setMetadata, - } = useCurrentDocument(); - const { documentsMap, getDocuments, updateDocumentContent } = useDocuments(); - - // Use document store instead of SWR - const [versionedDocuments, setVersionedDocuments] = useState>( - [], - ); - - useEffect(() => { - if (artifact.documentId !== 'init' && artifact.status !== 'streaming') { - const currentDocuments = getDocuments(artifact.documentId); - if (currentDocuments) { - setVersionedDocuments(currentDocuments); - } - } - }, [documentsMap, artifact.documentId, artifact.status]); // Remove getDocuments from dependencies - - const [mode, setMode] = useState<'edit' | 'diff'>('edit'); - const [document, setDocument] = useState(null); - const [currentVersionIndex, setCurrentVersionIndex] = useState(-1); - - useEffect(() => { - if (versionedDocuments && versionedDocuments.length > 0) { - const mostRecentDocument = versionedDocuments.at(-1); - - if (mostRecentDocument) { - setDocument(mostRecentDocument); - setCurrentVersionIndex(versionedDocuments.length - 1); - - // Only update if content is actually different to prevent infinite loops - setCurrentDocument((currentDocument) => { - if (currentDocument.content !== (mostRecentDocument.content ?? '')) { - return { - ...currentDocument, - content: mostRecentDocument.content ?? '', - }; - } - return currentDocument; - }); - } - } - }, [versionedDocuments]); // Remove setCurrentDocument from dependencies - - const [isContentDirty, setIsContentDirty] = useState(false); - - const handleContentChange = useCallback( - (updatedContent: string) => { - if (!artifact || !document) return; - - if (document.content !== updatedContent) { - // Update document in local store - updateDocumentContent(artifact.documentId, updatedContent); - - setIsContentDirty(false); - - // Update local state - update the current document in the array instead of replacing the whole array - setVersionedDocuments((prevVersions) => { - const updatedVersions = [...prevVersions]; - const currentIndex = updatedVersions.findIndex( - (doc) => doc.id === document.id, - ); - - if (currentIndex !== -1) { - updatedVersions[currentIndex] = { - ...document, - content: updatedContent, - updatedAt: Date.now(), - }; - } - - return updatedVersions; - }); - } - }, - [artifact, document, updateDocumentContent], - ); - - const debouncedHandleContentChange = useDebounceCallback( - handleContentChange, - 2000, - ); - - const saveContent = useCallback( - (updatedContent: string, debounce: boolean) => { - if (document && updatedContent !== document.content) { - setIsContentDirty(true); - - if (debounce) { - debouncedHandleContentChange(updatedContent); - } else { - handleContentChange(updatedContent); - } - } - }, - [document, debouncedHandleContentChange, handleContentChange], - ); - - function getDocumentContentById(index: number) { - if (!versionedDocuments) return ''; - if (!versionedDocuments[index]) return ''; - return versionedDocuments[index].content ?? ''; - } - - const handleVersionChange = (type: 'next' | 'prev' | 'toggle' | 'latest') => { - if (!versionedDocuments) return; - - if (type === 'latest') { - setCurrentVersionIndex(versionedDocuments.length - 1); - setMode('edit'); - } - - if (type === 'toggle') { - setMode((mode) => (mode === 'edit' ? 'diff' : 'edit')); - } - - if (type === 'prev') { - if (currentVersionIndex > 0) { - setCurrentVersionIndex((index) => index - 1); - } - } else if (type === 'next') { - if (currentVersionIndex < versionedDocuments.length - 1) { - setCurrentVersionIndex((index) => index + 1); - } - } - }; - - /* - * NOTE: if there are no documents, or if - * the documents are being fetched, then - * we mark it as the current version. - */ - - const isCurrentVersion = - versionedDocuments && versionedDocuments.length > 0 - ? currentVersionIndex === versionedDocuments.length - 1 - : true; - - const artifactDefinition = artifactDefinitions.find( - (definition) => definition.kind === artifact.kind, - ); - - if (!artifactDefinition) { - throw new Error('Artifact definition not found!'); - } - - useEffect(() => { - if (artifact.documentId !== 'init') { - if (artifactDefinition.initialize) { - artifactDefinition.initialize({ - documentId: artifact.documentId, - setMetadata, - }); - } - } - }, [artifact.documentId, artifactDefinition, setMetadata]); - - return ( -
-
-
- - -
-
{artifact.title}
- - {isContentDirty ? ( -
- Saving changes... -
- ) : document ? ( -
- {`Updated ${formatDistance( - new Date(document.updatedAt), - new Date(), - { - addSuffix: true, - }, - )}`} -
- ) : ( -
- )} -
-
- - -
- -
- -
- - {!isCurrentVersion && ( - - )} -
- ); -} diff --git a/src/features/documents/components/artifact.tsx b/src/features/documents/components/artifact.tsx deleted file mode 100644 index 22ef51c0..00000000 --- a/src/features/documents/components/artifact.tsx +++ /dev/null @@ -1,108 +0,0 @@ -'use client'; - -import type { Attachment, UIMessage } from 'ai'; -import { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { Messages, MultimodalInput } from '@/features/ai-chat/components'; -import { useChatDefault } from '@/features/ai-chat/hooks/use-chat-default'; -import { useCurrentDocument } from '@/features/documents/hooks/use-document-current'; -import { - ResizableHandle, - ResizablePanel, - ResizablePanelGroup, -} from '@/shared/components/ui/resizable'; -import { ArtifactMessagesHeader } from './artifact-messages-header'; -import { ArtifactViewer } from './artifact-viewer'; - -export function Artifact({ - chatId, - initialMessages, - isReadonly, -}: { - chatId: string; - initialMessages: Array; - isReadonly: boolean; -}) { - const navigate = useNavigate(); - - const handleOnResponse = (response: any) => { - navigate(`/artifact?cid=${chatId}`); - }; - - const { - messages, - setMessages: setChatMessages, - handleSubmit, - input, - setInput, - append, - status, - stop, - reload, - } = useChatDefault(chatId, initialMessages, handleOnResponse); - - const [attachments, setAttachments] = useState>([]); - const { currentDocument } = useCurrentDocument(); - const isArtifact = currentDocument.documentId !== 'init'; - - if (!isArtifact) { - return ( -
- Todo: this is the all artifact page -
- ); - } - - return ( -
- - {/* Artifact viewer panel */} - - - - - {/* Resizable handle */} - - - {/* Chat panel */} - -
- - - -
- {!isReadonly && ( - - )} - -
-
-
-
- ); -} diff --git a/src/features/documents/components/document-preview-call.tsx b/src/features/documents/components/document-preview-call.tsx deleted file mode 100644 index aa6a6d43..00000000 --- a/src/features/documents/components/document-preview-call.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { FileIcon, LoaderIcon, PencilIcon } from 'lucide-react'; -import { memo } from 'react'; -import { useNavigate } from 'react-router-dom'; - -import { toast } from '@/shared/components/toast'; - -import { useLanguage } from '@/shared/hooks/use-language'; - -const getActionText = ( - type: 'create' | 'update' | 'request-suggestions', - tense: 'present' | 'past', - t: (key: string) => string, -) => { - switch (type) { - case 'create': - return tense === 'present' - ? t('documentTool.creating') - : t('documentTool.created'); - case 'update': - return tense === 'present' - ? t('documentTool.updating') - : t('documentTool.updated'); - case 'request-suggestions': - return tense === 'present' - ? t('documentTool.addingSuggestions') - : t('documentTool.addedSuggestions'); - default: - return null; - } -}; - -interface DocumentToolCallProps { - chatId: string; - type: 'create' | 'update' | 'request-suggestions'; - args: { title: string }; - isReadonly: boolean; -} - -function PureDocumentToolCall({ - chatId, - type, - args, - isReadonly, -}: DocumentToolCallProps) { - const { t } = useLanguage(); - const navigate = useNavigate(); - - return ( - - ); -} - -export const DocumentToolCall = memo(PureDocumentToolCall, () => true); diff --git a/src/features/documents/components/document-preview-result.tsx b/src/features/documents/components/document-preview-result.tsx deleted file mode 100644 index 22b185df..00000000 --- a/src/features/documents/components/document-preview-result.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { FileIcon, PencilIcon } from 'lucide-react'; -import { memo } from 'react'; -import { useNavigate } from 'react-router-dom'; -import type { ArtifactKind } from '@/features/documents/artifacts'; -import { useCurrentDocument } from '@/features/documents/hooks/use-document-current'; -import { toast } from '@/shared/components/toast'; -import { useLanguage } from '@/shared/hooks/use-language'; - -const getActionText = ( - type: 'create' | 'update' | 'request-suggestions', - tense: 'present' | 'past', - t: (key: string) => string, -) => { - switch (type) { - case 'create': - return tense === 'present' - ? t('documentTool.creating') - : t('documentTool.created'); - case 'update': - return tense === 'present' - ? t('documentTool.updating') - : t('documentTool.updated'); - case 'request-suggestions': - return tense === 'present' - ? t('documentTool.addingSuggestions') - : t('documentTool.addedSuggestions'); - default: - return null; - } -}; - -interface DocumentToolResultProps { - chatId: string; - type: 'create' | 'update' | 'request-suggestions'; - result: { id: string; title: string; kind: ArtifactKind }; - isReadonly: boolean; -} - -function PureDocumentToolResult({ - chatId, - type, - result, - isReadonly, -}: DocumentToolResultProps) { - const { setCurrentDocument } = useCurrentDocument(); - const { t } = useLanguage(); - const navigate = useNavigate(); - - return ( - - ); -} - -export const DocumentToolResult = memo(PureDocumentToolResult, () => true); diff --git a/src/features/documents/components/document-preview.tsx b/src/features/documents/components/document-preview.tsx deleted file mode 100644 index a55e4707..00000000 --- a/src/features/documents/components/document-preview.tsx +++ /dev/null @@ -1,262 +0,0 @@ -'use client'; -import equal from 'fast-deep-equal'; -import { FileIcon, FullscreenIcon, ImageIcon, LoaderIcon } from 'lucide-react'; -import { memo, useCallback, useMemo, useRef } from 'react'; -import { useNavigate } from 'react-router-dom'; -import type { ArtifactKind } from '@/features/documents/artifacts'; -import { CodePreview } from '@/features/documents/artifacts/code/components/code-preview'; -import { ImagePreview } from '@/features/documents/artifacts/image/components/image-preview'; -import { SheetPreview } from '@/features/documents/artifacts/sheet/components/sheet-preview'; -import { TextPreview } from '@/features/documents/artifacts/text/components/text-preview'; -import { useCurrentDocument } from '@/features/documents/hooks/use-document-current'; -import { useDocuments } from '@/features/documents/hooks/use-documents'; - -import type { - CurrentDocumentProps, - Document, -} from '@/features/documents/stores'; -import { cn } from '@/shared/utils'; -import { DocumentToolCall } from './document-preview-call'; -import { DocumentToolResult } from './document-preview-result'; - -interface DocumentPreviewProps { - chatId: string; - isReadonly: boolean; - result?: any; - args?: any; -} - -export function DocumentPreview({ - chatId, - isReadonly, - result, - args, -}: DocumentPreviewProps) { - const { currentDocument: artifact, setCurrentDocument } = - useCurrentDocument(); - const { getDocument } = useDocuments(); - - // Use document store instead of SWR - const documents = useMemo(() => { - if (result?.id) { - const document = getDocument(result.id); - return document ? [document] : []; - } - return []; - }, [result?.id, getDocument]); - - const isDocumentsFetching = false; // No longer fetching from API - - const previewDocument = useMemo(() => documents?.[0], [documents]); - const hitboxRef = useRef(null); - - if (artifact.documentId !== 'init') { - if (result) { - return ( - - ); - } - - if (args) { - return ( - - ); - } - } - - if (isDocumentsFetching) { - return ; - } - - const document: Document | null = previewDocument - ? previewDocument - : artifact.status === 'streaming' - ? { - title: artifact.title, - kind: artifact.kind, - content: artifact.content, - id: artifact.documentId, - createdAt: Date.now(), - updatedAt: Date.now(), - } - : null; - - if (!document) return ; - - return ( -
- - - -
- ); -} - -const LoadingSkeleton = ({ artifactKind }: { artifactKind: ArtifactKind }) => ( -
-
-
-
-
-
-
-
-
- -
-
- {artifactKind === 'image' ? ( -
-
-
- ) : ( -
-
-
-
-
-
-
-
-
-
-
- )} -
-); - -const PureHitboxLayer = ({ - hitboxRef, - result, - setCurrentDocument, - chatId, -}: { - hitboxRef: React.RefObject; - result: any; - setCurrentDocument: ( - updaterFn: - | CurrentDocumentProps - | ((currentDocument: CurrentDocumentProps) => CurrentDocumentProps), - ) => void; - chatId: string; -}) => { - const navigate = useNavigate(); - const handleClick = useCallback(() => { - setCurrentDocument((artifact) => - artifact.status === 'streaming' - ? { ...artifact } - : { - ...artifact, - title: result.title, - documentId: result.id, - kind: result.kind, - }, - ); - navigate(`/artifact?cid=${chatId}`); - }, [setCurrentDocument, result, chatId, navigate]); - - return ( - - ); -}; - -const HitboxLayer = memo(PureHitboxLayer, (prevProps, nextProps) => { - if (!equal(prevProps.result, nextProps.result)) return false; - return true; -}); - -const PureDocumentHeader = ({ - title, - kind, - isStreaming, -}: { - title: string; - kind: ArtifactKind; - isStreaming: boolean; -}) => ( -
-
-
- {isStreaming ? ( -
- -
- ) : kind === 'image' ? ( - - ) : ( - - )} -
-
{title}
-
-
-
-); - -const DocumentHeader = memo(PureDocumentHeader, (prevProps, nextProps) => { - if (prevProps.title !== nextProps.title) return false; - if (prevProps.isStreaming !== nextProps.isStreaming) return false; - - return true; -}); - -const DocumentContentPreview = ({ document }: { document: Document }) => { - const { currentDocument: artifact } = useCurrentDocument(); - - const containerClassName = cn( - 'h-[257px] overflow-y-scroll border rounded-b-2xl dark:bg-muted border-t-0 dark:border-zinc-700', - { - 'p-4 sm:px-14 sm:py-16': document.kind === 'text', - 'p-0': document.kind === 'code', - }, - ); - - // Map artifact status to editor status - const editorStatus: 'streaming' | 'idle' = - artifact.status === 'streaming' ? 'streaming' : 'idle'; - - return ( -
- {document.kind === 'text' ? ( - - ) : document.kind === 'code' ? ( - - ) : document.kind === 'sheet' ? ( - - ) : document.kind === 'image' ? ( - - ) : null} -
- ); -}; diff --git a/src/features/documents/components/index.ts b/src/features/documents/components/index.ts deleted file mode 100644 index 2ea8cebd..00000000 --- a/src/features/documents/components/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export * from './artifact'; -export * from './artifact-actions'; -export * from './artifact-close-button'; -export * from './artifact-viewer'; -export * from './document-preview'; -export * from './document-preview-call'; -export * from './document-preview-result'; -export * from './version-footer'; diff --git a/src/features/documents/components/version-footer.tsx b/src/features/documents/components/version-footer.tsx deleted file mode 100644 index f51218c0..00000000 --- a/src/features/documents/components/version-footer.tsx +++ /dev/null @@ -1,76 +0,0 @@ -'use client'; - -import { motion } from 'framer-motion'; -import { LoaderIcon } from 'lucide-react'; -import { useWindowSize } from 'usehooks-ts'; -import { useCurrentDocument } from '@/features/documents/hooks/use-document-current'; -import { useVersionManagement } from '@/features/documents/hooks/use-version-management'; -import type { Document } from '@/features/documents/stores'; -import { Button } from '@/shared/components/ui/button'; -import { useLanguage } from '@/shared/hooks/use-language'; - -interface VersionFooterProps { - handleVersionChange: (type: 'next' | 'prev' | 'toggle' | 'latest') => void; - documents: Array | undefined; - currentVersionIndex: number; -} - -export const VersionFooter = ({ - handleVersionChange, - documents, - currentVersionIndex, -}: VersionFooterProps) => { - const { currentDocument: artifact } = useCurrentDocument(); - const { isMutating, restoreToVersion } = useVersionManagement(); - const { width } = useWindowSize(); - const isMobile = width < 768; - const { t } = useLanguage(); - - if (!documents) return; - - return ( - -
-
{t('version.viewingPrevious')}
-
- {t('version.restoreToEdit')} -
-
- -
- - -
-
- ); -}; diff --git a/src/features/documents/hooks/index.ts b/src/features/documents/hooks/index.ts deleted file mode 100644 index 7eeb43cd..00000000 --- a/src/features/documents/hooks/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './use-document'; -export * from './use-document-current'; -export * from './use-documents'; -export * from './use-version-management'; diff --git a/src/features/documents/hooks/use-document-current.ts b/src/features/documents/hooks/use-document-current.ts deleted file mode 100644 index d21e7888..00000000 --- a/src/features/documents/hooks/use-document-current.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { useCallback } from 'react'; -import { - DocumentStateStore, - defaultEmptyDocument, -} from '@/features/documents/stores'; -import type { CurrentDocumentProps } from '@/features/documents/types'; - -export const useCurrentDocument = () => { - const store = DocumentStateStore(); - - const setCurrentDocument = useCallback( - ( - updaterFn: - | CurrentDocumentProps - | ((currentDocument: CurrentDocumentProps) => CurrentDocumentProps), - ) => { - store.setCurrentDocument(updaterFn); - }, - [store], - ); - - const closeCurrentDocument = useCallback(() => { - setCurrentDocument((currentDocument) => - currentDocument.status === 'streaming' - ? { - ...currentDocument, - } - : { ...defaultEmptyDocument, status: 'idle' }, - ); - }, [store]); - - const updateCurrentDocument = useCallback( - (updates: Partial) => { - store.UpdateCurrentDocument(updates); - }, - [store], - ); - - const setMetadata = useCallback( - (metadata: any) => { - store.setCurrentDocumentMetadata(metadata); - }, - [store], - ); - - const resetCurrentDocument = useCallback(() => { - store.resetCurrentDocument(); - }, []); - - return { - currentDocument: store.currentDocument, - setCurrentDocument, - updateCurrentDocument, - metadata: store.getCurrentDocumentMetadata(), - setMetadata, - resetCurrentDocument, - closeCurrentDocument, - }; -}; diff --git a/src/features/documents/hooks/use-document.ts b/src/features/documents/hooks/use-document.ts deleted file mode 100644 index 780cb761..00000000 --- a/src/features/documents/hooks/use-document.ts +++ /dev/null @@ -1,42 +0,0 @@ -'use client'; - -import { useCallback } from 'react'; -import { DocumentStateStore } from '@/features/documents/stores'; -import { useDocuments } from './use-documents'; - -export const useDocument = (id: string) => { - const store = DocumentStateStore(); - const { - updateDocumentContent, - deleteDocument, - addNewVersion, - deleteAfterTimestamp, - } = useDocuments(); - - const document = store.getDocument(id); - - return { - document, - updateContent: useCallback( - (content: string) => { - updateDocumentContent(id, content); - }, - [updateDocumentContent, id], - ), - deleteDocument: useCallback(() => { - deleteDocument(id); - }, [deleteDocument, id]), - addNewVersion: useCallback( - (content: string) => { - addNewVersion(id, content); - }, - [addNewVersion, id], - ), - deleteAfterTimestamp: useCallback( - (updates: { content: string; timestamp: number }) => { - deleteAfterTimestamp(id, updates); - }, - [deleteAfterTimestamp, id], - ), - }; -}; diff --git a/src/features/documents/hooks/use-documents.ts b/src/features/documents/hooks/use-documents.ts deleted file mode 100644 index 31ad5305..00000000 --- a/src/features/documents/hooks/use-documents.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { useCallback } from 'react'; -import { DocumentStateStore } from '@/features/documents/stores'; -import type { Document } from '@/features/documents/types'; -import { generateUUID } from '@/shared/utils'; - -// Documents management hook with business logic -export const useDocuments = () => { - const store = DocumentStateStore(); - - // 创建文档(使用服务层) - const createDocument = useCallback( - (title: string, kind: Document['kind']) => { - const id = generateUUID(); - store.createDocumentWithId(id, title, kind); - return id; - }, - [], - ); - - // 创建指定 ID 的文档 - const createDocumentWithId = useCallback( - (id: string, title: string, kind: Document['kind'], content?: string) => { - store.createDocumentWithId(id, title, kind, content); - }, - [], - ); - - // 更新文档内容 - const updateDocumentContent = useCallback( - (id: string, content: string) => { - const existingDocument = store.getDocument(id); - if (!existingDocument) return; - - const updatedDocument: Document = { - ...existingDocument, - content, - }; - - store.updateDocument(id, updatedDocument); - }, - [store], - ); - - // 删除文档 - const deleteDocument = useCallback( - (id: string) => { - store.deleteDocument(id); - }, - [store], - ); - - // 添加新版本文档 - const addNewVersion = useCallback( - (id: string, content: string) => { - const originalDocument = store.getDocument(id); - if (!originalDocument) return; - - const versionId = generateUUID(); - const now = Date.now(); - - const versionDocument: Document = { - ...originalDocument, - id: versionId, - content, - createdAt: now, - updatedAt: now, - }; - - store.updateDocument(versionId, versionDocument); - }, - [store], - ); - - // 删除指定时间戳后的文档版本 - const deleteAfterTimestamp = useCallback( - (id: string, updates: { content: string; timestamp: number }) => { - store.deleteDocumentAfterTimestamp(id, updates); - }, - [store], - ); - - // 获取排序后的文档 - const getSortedDocuments = useCallback(() => { - return Object.values(store.documents).sort( - (a, b) => b.updatedAt - a.updatedAt, - ); - }, [store.documents]); - - // 清空所有文档 - const clearAllDocuments = useCallback(() => { - store.clearAllDocuments(); - }, [store]); - - return { - documents: store.documents, - documentsMap: store.documents, - getDocument: store.getDocument, - getDocuments: store.getDocuments, - createDocument, - createDocumentWithId, - updateDocumentContent, - deleteDocument, - addNewVersion, - deleteAfterTimestamp, - getSortedDocuments, - clearAllDocuments, - }; -}; diff --git a/src/features/documents/hooks/use-version-management.ts b/src/features/documents/hooks/use-version-management.ts deleted file mode 100644 index 1d217a4d..00000000 --- a/src/features/documents/hooks/use-version-management.ts +++ /dev/null @@ -1,49 +0,0 @@ -'use client'; - -import { useCallback, useState } from 'react'; -import type { Document } from '@/features/documents/types'; -import { useLanguage } from '@/shared/hooks/use-language'; -import { useDocuments } from './use-documents'; - -// Version management hook for document operations -export const useVersionManagement = () => { - const [isMutating, setIsMutating] = useState(false); - const { deleteAfterTimestamp } = useDocuments(); - const { t } = useLanguage(); - - const restoreToVersion = useCallback( - async ( - documentId: string, - documents: Document[], - currentVersionIndex: number, - onSuccess?: () => void, - ) => { - setIsMutating(true); - - try { - const currentDocument = documents[currentVersionIndex]; - if (currentDocument) { - // Update document to the selected version's content - await deleteAfterTimestamp(documentId, { - content: currentDocument.content ?? '', - timestamp: currentDocument.createdAt, - }); - - // Call success callback - onSuccess?.(); - } - } catch (error) { - console.error(t('version.failedRestore'), error); - throw error; - } finally { - setIsMutating(false); - } - }, - [deleteAfterTimestamp, t], - ); - - return { - isMutating, - restoreToVersion, - }; -}; diff --git a/src/features/documents/stores/index.ts b/src/features/documents/stores/index.ts deleted file mode 100644 index 6e44c4f8..00000000 --- a/src/features/documents/stores/index.ts +++ /dev/null @@ -1,383 +0,0 @@ -// document-store.ts -// Store for managing documents, suggestions, and artifacts with unified storage -import { create } from 'zustand'; -import { persist } from 'zustand/middleware'; -import { NuwaIdentityKit } from '@/features/auth/services'; -import { generateUUID } from '@/shared/utils'; -import { createPersistConfig, db } from '@/storage'; -import type { CurrentDocumentProps, Document } from '../types'; - -// Re-export types for convenience -export type { CurrentDocumentProps, Document } from '../types'; - -// ================= Constants ================= // - -// Empty document -export const defaultEmptyDocument: CurrentDocumentProps = { - documentId: 'init', - content: '', - kind: 'text', - title: '', - status: 'idle', -}; - -// get current DID -const getCurrentDID = async () => { - const { getDid } = await NuwaIdentityKit(); - return await getDid(); -}; - -// ================= Database Reference ================= // - -const documentDB = db; - -// ================= Store State Interface ================= // - -interface DocumentStoreState { - documents: Record; // all documents that are loaded into the store - currentDocument: CurrentDocumentProps; // the current document that is being viewed - currentDocumentMetadata: Record; // metadata for the current document - - // Document management - createDocument: (title: string, kind: Document['kind']) => string; - createDocumentWithId: ( - id: string, - title: string, - kind: Document['kind'], - content?: string, - ) => void; - addNewVersionDocument: (id: string, content: string) => void; - getDocument: (id: string) => Document | null; - getDocuments: (id: string) => Document[]; - updateDocument: ( - id: string, - updates: Partial>, - ) => void; - deleteDocument: (id: string) => void; - deleteDocumentAfterTimestamp: ( - id: string, - updates: { content: string; timestamp: number }, - ) => void; - setDocumentContent: (id: string, content: string) => void; - - // Current document management - setCurrentDocument: ( - updaterFn: - | CurrentDocumentProps - | ((currentDocument: CurrentDocumentProps) => CurrentDocumentProps), - ) => void; - UpdateCurrentDocument: (updates: Partial) => void; - setCurrentDocumentMetadata: (metadata: any) => void; - getCurrentDocumentMetadata: () => any; - resetCurrentDocument: () => void; - - // Utility methods - getSortedDocuments: () => Document[]; - clearAllDocuments: () => void; - - // Data persistence - loadFromDB: () => Promise; - saveToDB: () => Promise; -} - -// ================= Persist Configuration ================= // - -const persistConfig = createPersistConfig({ - name: 'document-storage', - getCurrentDID: getCurrentDID, - partialize: (state) => ({ - documents: state.documents, - currentDocument: state.currentDocument, - currentDocumentMetadata: state.currentDocumentMetadata, - }), - onRehydrateStorage: () => (state?: DocumentStoreState) => { - if (state) { - state.loadFromDB(); - } - }, -}); - -// ================= Store Definition ================= // - -export const DocumentStateStore = create()( - persist( - (set, get) => ({ - // Store state - documents: {}, - currentDocument: defaultEmptyDocument, - currentDocumentMetadata: {}, - - // Document creation and management - createDocument: (title: string, kind: Document['kind']) => { - const id = generateUUID(); - const now = Date.now(); - - const newDocument: Document = { - id, - title, - content: null, - kind, - createdAt: now, - updatedAt: now, - }; - - set((state) => ({ - documents: { - ...state.documents, - [id]: newDocument, - }, - })); - - // save to IndexedDB asynchronously - get().saveToDB(); - return id; - }, - - createDocumentWithId: ( - id: string, - title: string, - kind: Document['kind'], - content?: string, - ) => { - const now = Date.now(); - - const newDocument: Document = { - id, - title, - content: content || null, - kind, - createdAt: now, - updatedAt: now, - }; - - set((state) => ({ - documents: { - ...state.documents, - [id]: newDocument, - }, - })); - - // save to IndexedDB asynchronously - get().saveToDB(); - }, - - addNewVersionDocument: (id: string, content: string) => { - const document = get().getDocument(id); - if (!document) return; - const tableId = generateUUID(); - - set((state) => ({ - documents: { - ...state.documents, - [tableId]: { - ...document, - content, - createdAt: Date.now(), - updatedAt: Date.now(), - }, - }, - })); - }, - - getDocument: (id: string) => { - const { documents } = get(); - return documents[id] || null; - }, - - getDocuments: (id: string) => { - const { documents } = get(); - return Object.values(documents).filter( - (document) => document.id === id, - ); - }, - - updateDocument: ( - id: string, - updates: Partial>, - ) => { - set((state) => { - const document = state.documents[id]; - if (!document) return state; - - const updatedDocument = { - ...document, - ...updates, - updatedAt: Date.now(), - }; - - return { - documents: { - ...state.documents, - [id]: updatedDocument, - }, - }; - }); - - get().saveToDB(); - }, - - deleteDocumentAfterTimestamp: async ( - id: string, - updates: { content: string; timestamp: number }, - ) => { - set((state) => { - const documents = state.documents; - const newDocuments = Object.fromEntries( - Object.entries(documents).filter( - ([_, document]) => - document.id !== id || document.createdAt <= updates.timestamp, - ), - ); - newDocuments[id] = { - ...state.documents[id], - content: updates.content, - updatedAt: Date.now(), - }; - - return { - ...state, - documents: newDocuments, - }; - }); - - get().saveToDB(); - }, - - deleteDocument: (id: string) => { - set((state) => { - const { [id]: deleted, ...restDocuments } = state.documents; - - return { - documents: restDocuments, - }; - }); - - // delete related data asynchronously - const deleteFromDB = async () => { - try { - await documentDB.documents.delete(id); - } catch (error) { - console.error('Failed to delete from DB:', error); - } - }; - deleteFromDB(); - }, - - setDocumentContent: (id: string, content: string) => { - get().updateDocument(id, { content }); - }, - - // Artifact management methods (merged from use-artifact.ts) - setCurrentDocument: ( - updaterFn: - | CurrentDocumentProps - | ((currentDocument: CurrentDocumentProps) => CurrentDocumentProps), - ) => { - set((state) => { - const newArtifact = - typeof updaterFn === 'function' - ? updaterFn(state.currentDocument) - : updaterFn; - - return { - currentDocument: newArtifact, - }; - }); - }, - - UpdateCurrentDocument: (updates: Partial) => { - set((state) => ({ - currentDocument: { - ...state.currentDocument, - ...updates, - }, - })); - }, - - setCurrentDocumentMetadata: (metadata: any) => { - set((state) => ({ - currentDocumentMetadata: { - ...state.currentDocumentMetadata, - [state.currentDocument.documentId]: metadata, - }, - })); - }, - - getCurrentDocumentMetadata: () => { - const { currentDocumentMetadata, currentDocument } = get(); - return currentDocumentMetadata[currentDocument.documentId] || null; - }, - - resetCurrentDocument: () => { - set({ - currentDocument: defaultEmptyDocument, - }); - }, - - getSortedDocuments: () => { - const { documents } = get(); - return Object.values(documents).sort( - (a, b) => b.updatedAt - a.updatedAt, - ); - }, - - clearAllDocuments: () => { - set({ - documents: {}, - }); - - // clear IndexedDB - const clearDB = async () => { - try { - const currentDID = await getCurrentDID(); - if (!currentDID) return; - - await documentDB.documents.where('did').equals(currentDID).delete(); - } catch (error) { - console.error('Failed to clear documents from DB:', error); - } - }; - clearDB(); - }, - - loadFromDB: async () => { - if (typeof window === 'undefined') return; - - try { - const currentDID = await getCurrentDID(); - if (!currentDID) return; - - const documents = await documentDB.documents - .where('did') - .equals(currentDID) - .toArray(); - - const documentsMap: Record = {}; - - documents.forEach((doc: Document) => { - documentsMap[doc.id] = doc; - }); - - set((state) => ({ - documents: { ...state.documents, ...documentsMap }, - })); - } catch (error) { - console.error('Failed to load from DB:', error); - } - }, - - saveToDB: async () => { - if (typeof window === 'undefined') return; - - try { - const { documents } = get(); - const documentsToSave = Object.values(documents); - await documentDB.documents.bulkPut(documentsToSave); - } catch (error) { - console.error('Failed to save to DB:', error); - } - }, - }), - persistConfig, - ), -); diff --git a/src/features/documents/types/index.ts b/src/features/documents/types/index.ts deleted file mode 100644 index 16c8b68d..00000000 --- a/src/features/documents/types/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -// Document interface -export interface Document { - id: string; - did?: string; - title: string; - content: string | null; - kind: 'text' | 'code' | 'image' | 'sheet'; - createdAt: number; - updatedAt: number; -} - -export interface CurrentDocumentProps { - documentId: string; - content: string; - kind: 'text' | 'code' | 'image' | 'sheet'; - title: string; - status: 'streaming' | 'idle' | 'loading' | 'error' | 'success'; -} diff --git a/src/layout/components/app-sidebar.tsx b/src/layout/components/app-sidebar.tsx index 7bb0bdff..5d566cf0 100644 --- a/src/layout/components/app-sidebar.tsx +++ b/src/layout/components/app-sidebar.tsx @@ -1,6 +1,6 @@ 'use client'; -import { Bug, Folder, Package, Search, Settings } from 'lucide-react'; +import { Bug, Package, Search, Settings } from 'lucide-react'; import { useNavigate } from 'react-router-dom'; import { CapStoreModal } from '@/features/cap/components'; import { SearchModal } from '@/features/search/components'; @@ -89,14 +89,6 @@ export function AppSidebar() { /> )} - {isDevMode && ( - {}} - variant="secondary" - /> - )} {isDevMode && ( ; - } - - return ( - - ); -} diff --git a/src/pages/index.ts b/src/pages/index.ts index fb48ae30..9ac1e730 100644 --- a/src/pages/index.ts +++ b/src/pages/index.ts @@ -1,4 +1,3 @@ -export * from './artifact'; export * from './callback'; export * from './chat'; export * from './error'; diff --git a/src/router.tsx b/src/router.tsx index ac133c18..dfe1caef 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -1,7 +1,6 @@ import { createBrowserRouter, Navigate } from 'react-router-dom'; import MainLayout from './layout/main-layout'; import RootLayout from './layout/root-layout'; -import ArtifactPage from './pages/artifact'; import CallbackPage from './pages/callback'; import ChatPage from './pages/chat'; import ErrorPage from './pages/error'; @@ -19,7 +18,6 @@ const router = createBrowserRouter([ errorElement: , children: [ { index: true, element: }, - { path: 'artifact', element: }, { path: 'chat', element: }, { path: 'mcp-debug', element: }, ], diff --git a/src/shared/hooks/use-storage.ts b/src/shared/hooks/use-storage.ts index c92ef40a..1899452d 100644 --- a/src/shared/hooks/use-storage.ts +++ b/src/shared/hooks/use-storage.ts @@ -3,7 +3,6 @@ import { FileStateStore, MemoryStateStore, } from '@/features/ai-chat/stores'; -import { DocumentStateStore } from '@/features/documents/stores'; import { SettingsStateStore } from '@/features/settings/stores'; // Check if we're in a browser environment @@ -20,7 +19,6 @@ export const useStorage = () => { ChatStateStore.persist.clearStorage(); SettingsStateStore.persist.clearStorage(); FileStateStore.persist.clearStorage(); - DocumentStateStore.persist.clearStorage(); MemoryStateStore.persist.clearStorage(); // clear localStorage @@ -43,7 +41,6 @@ export const useStorage = () => { ChatStateStore.setState(ChatStateStore.getInitialState()); SettingsStateStore.setState(SettingsStateStore.getInitialState()); FileStateStore.setState(FileStateStore.getInitialState()); - DocumentStateStore.setState(DocumentStateStore.getInitialState()); MemoryStateStore.setState(MemoryStateStore.getInitialState()); }; @@ -51,7 +48,6 @@ export const useStorage = () => { ChatStateStore.persist.rehydrate(); SettingsStateStore.persist.rehydrate(); FileStateStore.persist.rehydrate(); - DocumentStateStore.persist.rehydrate(); MemoryStateStore.persist.rehydrate(); }; diff --git a/src/storage/actions.ts b/src/storage/actions.ts index 02ccaa6d..dfb65cb4 100644 --- a/src/storage/actions.ts +++ b/src/storage/actions.ts @@ -92,7 +92,6 @@ class StorageActions { 'rw', [ db.chats, - db.documents, db.files, db.fileData, db.streams, @@ -102,7 +101,6 @@ class StorageActions { async () => { await Promise.all([ db.chats.clear(), - db.documents.clear(), db.files.clear(), db.fileData.clear(), db.streams.clear(), diff --git a/src/storage/db.ts b/src/storage/db.ts index 2ab37169..48a17c23 100644 --- a/src/storage/db.ts +++ b/src/storage/db.ts @@ -2,7 +2,6 @@ import Dexie, { type Table } from 'dexie'; class Database extends Dexie { chats!: Table; - documents!: Table; files!: Table; fileData!: Table<{ id: string; blob: Blob }>; streams!: Table; @@ -21,7 +20,6 @@ class Database extends Dexie { this.version(1).stores({ chats: 'id, did, createdAt, updatedAt', - documents: 'id, did, createdAt, updatedAt', files: 'id, did, createdAt, updatedAt', fileData: 'id', streams: 'id, did, chatId, createdAt', From fceb4b3fcc8b239d44802489ca561b7e018aa864 Mon Sep 17 00:00:00 2001 From: Mine77 Date: Wed, 23 Jul 2025 19:21:55 +0800 Subject: [PATCH 06/85] refactor: remove the layout component; create the sidebar components --- .../ai-chat}/components/assistant-nav.tsx | 0 src/features/ai-chat/components/chat.tsx | 6 ++---- .../ai-chat}/components/header.tsx | 0 .../ai-chat}/components/path-breadcrumb.tsx | 0 src/features/search/components/index.ts | 1 - .../sidebar}/components/app-sidebar.tsx | 2 +- .../sidebar}/components/floating-sidebar.tsx | 0 src/features/sidebar/components/index.ts | 1 + .../components/search-modal.tsx | 0 .../sidebar}/components/sidebar-button.tsx | 0 .../components/sidebar-history-item.tsx | 0 .../sidebar}/components/sidebar-history.tsx | 0 .../sidebar}/components/sidebar-toggle.tsx | 0 .../{search => sidebar}/hooks/use-search.ts | 0 src/layout/components/index.ts | 9 --------- src/layout/components/loading.tsx | 17 ----------------- src/layout/main-layout.tsx | 4 ++-- 17 files changed, 6 insertions(+), 34 deletions(-) rename src/{layout => features/ai-chat}/components/assistant-nav.tsx (100%) rename src/{layout => features/ai-chat}/components/header.tsx (100%) rename src/{layout => features/ai-chat}/components/path-breadcrumb.tsx (100%) delete mode 100644 src/features/search/components/index.ts rename src/{layout => features/sidebar}/components/app-sidebar.tsx (98%) rename src/{layout => features/sidebar}/components/floating-sidebar.tsx (100%) create mode 100644 src/features/sidebar/components/index.ts rename src/features/{search => sidebar}/components/search-modal.tsx (100%) rename src/{layout => features/sidebar}/components/sidebar-button.tsx (100%) rename src/{layout => features/sidebar}/components/sidebar-history-item.tsx (100%) rename src/{layout => features/sidebar}/components/sidebar-history.tsx (100%) rename src/{layout => features/sidebar}/components/sidebar-toggle.tsx (100%) rename src/features/{search => sidebar}/hooks/use-search.ts (100%) delete mode 100644 src/layout/components/index.ts delete mode 100644 src/layout/components/loading.tsx diff --git a/src/layout/components/assistant-nav.tsx b/src/features/ai-chat/components/assistant-nav.tsx similarity index 100% rename from src/layout/components/assistant-nav.tsx rename to src/features/ai-chat/components/assistant-nav.tsx diff --git a/src/features/ai-chat/components/chat.tsx b/src/features/ai-chat/components/chat.tsx index 4c830d85..8055939b 100644 --- a/src/features/ai-chat/components/chat.tsx +++ b/src/features/ai-chat/components/chat.tsx @@ -1,9 +1,9 @@ -'use client';; +'use client'; import type { Attachment, UIMessage } from 'ai'; import { useState } from 'react'; import { useChatDefault } from '@/features/ai-chat/hooks/use-chat-default'; -import Header from '@/layout/components/header'; +import Header from './header'; import { Messages } from './messages'; import { MultimodalInput } from './multimodal-input'; @@ -16,7 +16,6 @@ export function Chat({ initialMessages: Array; isReadonly: boolean; }) { - const { messages, setMessages: setChatMessages, @@ -33,7 +32,6 @@ export function Chat({ return (
- {/* Chat */}
diff --git a/src/layout/components/header.tsx b/src/features/ai-chat/components/header.tsx similarity index 100% rename from src/layout/components/header.tsx rename to src/features/ai-chat/components/header.tsx diff --git a/src/layout/components/path-breadcrumb.tsx b/src/features/ai-chat/components/path-breadcrumb.tsx similarity index 100% rename from src/layout/components/path-breadcrumb.tsx rename to src/features/ai-chat/components/path-breadcrumb.tsx diff --git a/src/features/search/components/index.ts b/src/features/search/components/index.ts deleted file mode 100644 index 7ad62c48..00000000 --- a/src/features/search/components/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { SearchModal } from './search-modal'; diff --git a/src/layout/components/app-sidebar.tsx b/src/features/sidebar/components/app-sidebar.tsx similarity index 98% rename from src/layout/components/app-sidebar.tsx rename to src/features/sidebar/components/app-sidebar.tsx index 5d566cf0..eb8adbfb 100644 --- a/src/layout/components/app-sidebar.tsx +++ b/src/features/sidebar/components/app-sidebar.tsx @@ -3,7 +3,6 @@ import { Bug, Package, Search, Settings } from 'lucide-react'; import { useNavigate } from 'react-router-dom'; import { CapStoreModal } from '@/features/cap/components'; -import { SearchModal } from '@/features/search/components'; import { SettingsModal } from '@/features/settings/components'; import { useSidebarSettings } from '@/features/settings/hooks/use-settings-sidebar'; import { Logo } from '@/shared/components'; @@ -19,6 +18,7 @@ import { useDevMode } from '@/shared/hooks'; import { useLanguage } from '@/shared/hooks/use-language'; import { cn } from '@/shared/utils'; import { useFloatingSidebar } from './floating-sidebar'; +import { SearchModal } from './search-modal'; import { SidebarButton } from './sidebar-button'; import { SidebarHistory } from './sidebar-history'; import { SidebarToggle } from './sidebar-toggle'; diff --git a/src/layout/components/floating-sidebar.tsx b/src/features/sidebar/components/floating-sidebar.tsx similarity index 100% rename from src/layout/components/floating-sidebar.tsx rename to src/features/sidebar/components/floating-sidebar.tsx diff --git a/src/features/sidebar/components/index.ts b/src/features/sidebar/components/index.ts new file mode 100644 index 00000000..217682cc --- /dev/null +++ b/src/features/sidebar/components/index.ts @@ -0,0 +1 @@ +export * from './floating-sidebar'; \ No newline at end of file diff --git a/src/features/search/components/search-modal.tsx b/src/features/sidebar/components/search-modal.tsx similarity index 100% rename from src/features/search/components/search-modal.tsx rename to src/features/sidebar/components/search-modal.tsx diff --git a/src/layout/components/sidebar-button.tsx b/src/features/sidebar/components/sidebar-button.tsx similarity index 100% rename from src/layout/components/sidebar-button.tsx rename to src/features/sidebar/components/sidebar-button.tsx diff --git a/src/layout/components/sidebar-history-item.tsx b/src/features/sidebar/components/sidebar-history-item.tsx similarity index 100% rename from src/layout/components/sidebar-history-item.tsx rename to src/features/sidebar/components/sidebar-history-item.tsx diff --git a/src/layout/components/sidebar-history.tsx b/src/features/sidebar/components/sidebar-history.tsx similarity index 100% rename from src/layout/components/sidebar-history.tsx rename to src/features/sidebar/components/sidebar-history.tsx diff --git a/src/layout/components/sidebar-toggle.tsx b/src/features/sidebar/components/sidebar-toggle.tsx similarity index 100% rename from src/layout/components/sidebar-toggle.tsx rename to src/features/sidebar/components/sidebar-toggle.tsx diff --git a/src/features/search/hooks/use-search.ts b/src/features/sidebar/hooks/use-search.ts similarity index 100% rename from src/features/search/hooks/use-search.ts rename to src/features/sidebar/hooks/use-search.ts diff --git a/src/layout/components/index.ts b/src/layout/components/index.ts deleted file mode 100644 index d9aa8707..00000000 --- a/src/layout/components/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export * from './app-sidebar'; -export * from './assistant-nav'; -export * from './floating-sidebar'; -export * from './header'; -export * from './loading'; -export * from './path-breadcrumb'; -export * from './sidebar-history'; -export * from './sidebar-history-item'; -export * from './sidebar-toggle'; diff --git a/src/layout/components/loading.tsx b/src/layout/components/loading.tsx deleted file mode 100644 index d7f91367..00000000 --- a/src/layout/components/loading.tsx +++ /dev/null @@ -1,17 +0,0 @@ -'use client'; - -import { useLanguage } from '@/shared/hooks/use-language'; - -export default function Loading() { - const { t } = useLanguage(); - return ( -
-
-
-
-

{t('chat.loadingChat')}

-
-
-
- ); -} diff --git a/src/layout/main-layout.tsx b/src/layout/main-layout.tsx index 597e8ce8..d32899d3 100644 --- a/src/layout/main-layout.tsx +++ b/src/layout/main-layout.tsx @@ -1,8 +1,8 @@ 'use client'; import { Suspense } from 'react'; import { Outlet } from 'react-router-dom'; -import { FloatingSidebarLayout } from './components/floating-sidebar'; -import Loading from './components/loading'; +import { FloatingSidebarLayout } from '@/features/sidebar/components'; +import Loading from '@/shared/components/loading'; export default function MainLayout() { return ( From a585c88aa8547394bc9041305c6db5c1e01553a9 Mon Sep 17 00:00:00 2001 From: Mine77 Date: Wed, 23 Jul 2025 19:34:56 +0800 Subject: [PATCH 07/85] refacotr: organize code --- .gitignore | 5 +- package.json | 1 - src/caps/build.ts | 125 ------------------ src/caps/cn/image_generation.yaml | 42 ------ src/caps/cn/note.yaml | 93 ------------- src/features/ai-chat/stores/chat-store.ts | 8 +- src/features/ai-chat/stores/file-store.ts | 2 +- src/features/ai-chat/stores/memory-store.ts | 7 +- .../ai-chat/{types/index.ts => types.ts} | 0 .../ai-chat/{utils/message.ts => utils.ts} | 0 src/features/ai-chat/utils/index.ts | 1 - .../{stores/index.ts => stores.ts} | 6 +- .../ai-provider/{types/index.ts => types.ts} | 0 .../ai-provider/{utils/index.ts => utils.ts} | 2 +- src/features/auth/hooks/use-auth.ts | 2 - src/features/cap/stores/index.ts | 2 +- .../settings/{stores/index.ts => stores.ts} | 2 +- src/{ => shared}/storage/actions.ts | 0 src/{ => shared}/storage/db.ts | 0 src/{ => shared}/storage/helper.ts | 0 src/{ => shared}/storage/index.ts | 0 src/{ => shared}/storage/types.ts | 0 22 files changed, 18 insertions(+), 280 deletions(-) delete mode 100644 src/caps/build.ts delete mode 100644 src/caps/cn/image_generation.yaml delete mode 100644 src/caps/cn/note.yaml rename src/features/ai-chat/{types/index.ts => types.ts} (100%) rename src/features/ai-chat/{utils/message.ts => utils.ts} (100%) delete mode 100644 src/features/ai-chat/utils/index.ts rename src/features/ai-provider/{stores/index.ts => stores.ts} (97%) rename src/features/ai-provider/{types/index.ts => types.ts} (100%) rename src/features/ai-provider/{utils/index.ts => utils.ts} (98%) rename src/features/settings/{stores/index.ts => stores.ts} (98%) rename src/{ => shared}/storage/actions.ts (100%) rename src/{ => shared}/storage/db.ts (100%) rename src/{ => shared}/storage/helper.ts (100%) rename src/{ => shared}/storage/index.ts (100%) rename src/{ => shared}/storage/types.ts (100%) diff --git a/.gitignore b/.gitignore index 83720016..333ff956 100644 --- a/.gitignore +++ b/.gitignore @@ -46,4 +46,7 @@ playwright-report/ test-results/ # cap -public/caps.json \ No newline at end of file +public/caps.json + +# claude +.claude \ No newline at end of file diff --git a/package.json b/package.json index 9139c6d6..6de2f89f 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,6 @@ "scripts": { "dev": "vite", "build": "tsc -b && vite build", - "prebuild": "tsx src/caps/build.ts", "preview": "vite preview", "lint": "biome lint src/", "lint:fix": "biome lint --apply src/", diff --git a/src/caps/build.ts b/src/caps/build.ts deleted file mode 100644 index 0c9602ca..00000000 --- a/src/caps/build.ts +++ /dev/null @@ -1,125 +0,0 @@ -import fs from 'node:fs'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import yaml from 'js-yaml'; - -const __filename = fileURLToPath(import.meta.url); -const dirname = path.dirname(__filename); - -function findYamlFiles(dir: string): string[] { - const files: string[] = []; - const items = fs.readdirSync(dir); - - for (const item of items) { - const fullPath = path.join(dir, item); - const stat = fs.statSync(fullPath); - - if (stat.isDirectory()) { - files.push(...findYamlFiles(fullPath)); - } else if (item.endsWith('.yaml') || item.endsWith('.yml')) { - files.push(fullPath); - } - } - - return files; -} - -function generateRandomData() { - const authors = [ - 'AI Assistant Team', - 'CodeCraft Team', - 'Nuwa Labs', - 'Developer Tools', - 'AI Studio', - ]; - const tags = [ - 'development', - 'productivity', - 'design', - 'analytics', - 'security', - ]; - const versions = ['1.0.0', '1.1.0', '1.2.0', '2.0.0', '0.9.0']; - - return { - downloads: Math.floor(Math.random() * 5000) + 100, - version: versions[Math.floor(Math.random() * versions.length)], - author: authors[Math.floor(Math.random() * authors.length)], - tag: tags[Math.floor(Math.random() * tags.length)], - size: (Math.random() * 5 + 0.5).toFixed(1), - createdAt: - Date.now() - Math.floor(Math.random() * 90 + 10) * 24 * 60 * 60 * 1000, - updatedAt: - Date.now() - Math.floor(Math.random() * 30 + 1) * 24 * 60 * 60 * 1000, - }; -} - -function standardizeCapObject(yamlData: any, key: string, yamlPath: string) { - const randomData = generateRandomData(); - - return { - id: yamlData.metadata?.id || `cap:${key.replace(/\//g, ':')}`, - name: - yamlData.metadata?.name || - yamlData.name || - key.split('/').pop()?.replace(/_/g, ' ') || - 'Unknown Cap', - description: - yamlData.metadata?.description || - yamlData.description || - 'A useful AI assistant capability', - tag: randomData.tag, - downloads: randomData.downloads, - version: randomData.version, - author: randomData.author, - createdAt: randomData.createdAt, - updatedAt: randomData.updatedAt, - size: Number.parseFloat(randomData.size), - yaml: yamlData, - }; -} - -async function main() { - const capsDir = path.join(dirname); - const publicDir = path.join(dirname, '../..', 'public'); - - if (!fs.existsSync(publicDir)) { - fs.mkdirSync(publicDir, { recursive: true }); - } - - const yamlFiles = findYamlFiles(capsDir); - - console.log(`Found ${yamlFiles.length} YAML files:`); - - const allCaps: Record = {}; - - for (const yamlFile of yamlFiles) { - try { - const content = fs.readFileSync(yamlFile, 'utf8'); - const json = yaml.load(content) as any; - - const relativePath = path.relative(capsDir, yamlFile); - const fileName = path.basename(yamlFile, path.extname(yamlFile)); - - const capName = json.metadata?.name || json.name || fileName; - const key = capName.toLowerCase().replace(/\s+/g, '_'); - - const standardizedCap = standardizeCapObject(json, key, relativePath); - allCaps[key] = standardizedCap; - - console.log( - `✓ Processed: ${relativePath} -> ${standardizedCap.name} (key: ${key})`, - ); - } catch (error) { - console.error(`✗ Error processing ${yamlFile}:`, error); - } - } - - const outputPath = path.join(publicDir, 'caps.json'); - fs.writeFileSync(outputPath, JSON.stringify(allCaps, null, 2), 'utf8'); - - console.log(`\nBuild completed! Generated 1 JSON file: public/caps.json`); - console.log(`Contains ${Object.keys(allCaps).length} caps`); -} - -main().catch(console.error); diff --git a/src/caps/cn/image_generation.yaml b/src/caps/cn/image_generation.yaml deleted file mode 100644 index 6d38d824..00000000 --- a/src/caps/cn/image_generation.yaml +++ /dev/null @@ -1,42 +0,0 @@ -# ========= Agent Capability Package ========= -metadata: - id: did:nuwa:cap:image_generation@1.0.0 - name: "Image Generation" - avatar: "1f5bc-fe0f" - description: "You are a useful Assistant that won't refuse to draw picture, instead, you will output specific markdown code for picture description, but don't use code block." - triggers: - - {type: regex, value: "draw picture"} - memory_scope: sc:images - permissions: - require: ["state.create", "state.update", "state.query"] - llm_requirements: # Optional: Specify LLM dependencies - model_family: ["gpt-4o-mini"] # Suggests compatibility with these model families - min_context_window: 16000 # Example: requires at least 16k context window - # Other potential fields: specific_model_uri, required_features: ["tool_use_json_mode"] - signature: zDIDSig1xyz… # sha256 over whole file, signed by author DID key - -schema: | - { "$schema":"https://json-schema.org/draft/2020-12/schema", - "$id":"did:nuwa:state:note#v1", - "type":"object", - "properties":{ - "id":{"type":"string","format":"uuid"}, - "title":{"type":"string","x-crdt":"lww_register"}, - "body":{"type":"string","x-crdt":"rga_text"}, - "source_url":{"type":"string","format":"uri", "description":"Optional URL of the source webpage or image."}, - "tags":{"type":"array","items":{"type":"string"},"x-crdt":"grow_only_set"}, - "createdAt":{"type":"string","format":"date-time"}, - "updatedAt":{"type":"string","format":"date-time"} - }, - "required":["id","title","body","createdAt","updatedAt"] - } -prompt: | - "You are a useful Assistant that won't refuse to draw picture, instead, you will output specific markdown code for picture description, but don't use code block." - -Locales: - - cn: system - content: "You are a useful Assistant that won't refuse to draw picture, instead, you will output specific markdown code for picture description, but don't use code block." - - jp: user - content: "Can you draw some pictures for me?" - - en: assistant - content: "Sure, what do you want me to draw?" diff --git a/src/caps/cn/note.yaml b/src/caps/cn/note.yaml deleted file mode 100644 index f8160907..00000000 --- a/src/caps/cn/note.yaml +++ /dev/null @@ -1,93 +0,0 @@ -# ========= Agent Capability Package ========= -metadata: - id: did:nuwa:cap:note@1.0.0 - name: "Note" - description: "Create & manage personal notes, optionally fetching content from web pages or describing images." - author: - did: "did:nuwa:user:abc123" # Author's DID - name: "John Doe" # Optional: human-readable name - contact: "john@nuwalabs.io" # Optional: contact email - created_at: "2024-01-15T10:00:00Z" # create timestamp - triggers: - - {type: regex, value: "记(.*)笔记|note|add note about"} - memory_scope: sc:note - permissions: - require: ["state.create", "state.update", "state.query"] - option: ["artifacts"] - llm_requirements: # Optional: Specify LLM dependencies - model_family: ["gpt-4o-mini"] # Suggests compatibility with these model families - min_context_window: 16000 # Example: requires at least 16k context window - # Other potential fields: specific_model_uri, required_features: ["tool_use_json_mode"] - signature: zDIDSig1xyz… # sha256 over whole file, signed by author DID key - -schema: | - { "$schema":"https://json-schema.org/draft/2020-12/schema", - "$id":"did:nuwa:state:note#v1", - "type":"object", - "properties":{ - "id":{"type":"string","format":"uuid"}, - "title":{"type":"string","x-crdt":"lww_register"}, - "body":{"type":"string","x-crdt":"rga_text"}, - "source_url":{"type":"string","format":"uri", "description":"Optional URL of the source webpage or image."}, - "tags":{"type":"array","items":{"type":"string"},"x-crdt":"grow_only_set"}, - "createdAt":{"type":"string","format":"date-time"}, - "updatedAt":{"type":"string","format":"date-time"} - }, - "required":["id","title","body","createdAt","updatedAt"] - } - -prompt: | - You are Note Assistant. - Your primary goal is to create a well-structured note object. - If the user provides a URL, consider using the `fetch_web_content` tool to get its content to include in the note body. - If the user provides an image URL, consider using the `recognize_image_content` tool to get a description to include in the note body. - After gathering all necessary information, transform it into a Note object that conforms to the schema. - Then call `state.create` with: - schema_uri = "did:nuwa:state:note#v1" - object = - If you use a tool like `fetch_web_content` or `recognize_image_content`, use its output to enrich the note's body. - Always set the `source_url` field in the note object if the note is about a specific webpage or image. - Reply only with the final `state.create` tool call, or an intermediate tool call if you need more information. - -tools: - - type: function - function: - name: state.create # built-in tool - description: Persist a new state object (a note). - parameters: - type: object - properties: - schema_uri: {type: string, enum: ["did:nuwa:state:note#v1"]} - object: {$ref: "#/schema"} - required: [schema_uri, object] - - type: function - function: - name: fetch_web_content - description: "Fetches the main textual content from a given web page URL. Useful for summarizing or taking notes about online articles." - parameters: - type: object - properties: - url: {type: string, format: uri, description: "The URL of the web page to fetch content from."} - required: [url] - - type: function - function: - name: recognize_image_content - description: "Analyzes an image from a given URL and returns a textual description of its content. Useful for adding context about an image to a note." - parameters: - type: object - properties: - image_url: {type: string, format: uri, description: "The URL of the image to analyze."} - required: [image_url] - -tool_bindings: - "fetch_web_content": - type: "mcp_service" - service_uri: "did:nuwa:mcp:webscraper:version1" # Example MCP service URI - mcp_action: "extract_text_content" - # Arguments from LLM tool call (e.g., {url: "..."}) are passed as payload to MCP action. - "recognize_image_content": - type: "mcp_service" - service_uri: "did:nuwa:mcp:visiondescribers:stable" # Example MCP service URI - mcp_action: "describe_image_from_url" - # Arguments from LLM tool call (e.g., {image_url: "..."}) are passed as payload. -# ========= End of ACP ========= \ No newline at end of file diff --git a/src/features/ai-chat/stores/chat-store.ts b/src/features/ai-chat/stores/chat-store.ts index b1f48cfd..cb747184 100644 --- a/src/features/ai-chat/stores/chat-store.ts +++ b/src/features/ai-chat/stores/chat-store.ts @@ -5,8 +5,8 @@ import type { Message } from 'ai'; import { create } from 'zustand'; import { persist } from 'zustand/middleware'; import { NuwaIdentityKit } from '@/features/auth/services'; +import { createPersistConfig, db } from '@/shared/storage'; import { generateUUID } from '@/shared/utils'; -import { createPersistConfig, db } from '@/storage'; import type { ChatSession } from '../types'; // ================= Constants ================= // @@ -44,7 +44,7 @@ interface ChatStoreState { // update messages for a session updateMessages: (sessionId: string, messages: Message[]) => void; - + // utility methods clearAllSessions: () => void; @@ -206,8 +206,6 @@ export const ChatStateStore = create()( get().saveToDB(); }, - - clearAllSessions: () => { set({ sessions: {}, @@ -271,4 +269,4 @@ export const ChatStateStore = create()( }), persistConfig, ), -); \ No newline at end of file +); diff --git a/src/features/ai-chat/stores/file-store.ts b/src/features/ai-chat/stores/file-store.ts index 4bf786e1..b140375b 100644 --- a/src/features/ai-chat/stores/file-store.ts +++ b/src/features/ai-chat/stores/file-store.ts @@ -3,8 +3,8 @@ import { create } from 'zustand'; import { persist } from 'zustand/middleware'; import { NuwaIdentityKit } from '@/features/auth/services'; +import { createPersistConfig, db } from '@/shared/storage'; import { generateUUID } from '@/shared/utils'; -import { createPersistConfig, db } from '@/storage'; // ================= Constants & Types ================= // diff --git a/src/features/ai-chat/stores/memory-store.ts b/src/features/ai-chat/stores/memory-store.ts index 2423be48..a8bc4031 100644 --- a/src/features/ai-chat/stores/memory-store.ts +++ b/src/features/ai-chat/stores/memory-store.ts @@ -9,8 +9,8 @@ import { import { create } from 'zustand'; import { persist } from 'zustand/middleware'; import { NuwaIdentityKit } from '@/features/auth/services'; +import { createPersistConfig, db } from '@/shared/storage'; import { generateUUID } from '@/shared/utils'; -import { createPersistConfig, db } from '@/storage'; env.allowLocalModels = false; @@ -166,8 +166,9 @@ export const MemoryStateStore = create()( // 使用单个索引查询并过滤结果 await memoryDB.memories - .where('did').equals(currentDID) - .and(item => item.id === id) + .where('did') + .equals(currentDID) + .and((item) => item.id === id) .delete(); } catch (error) { console.error('Failed to delete memory from DB:', error); diff --git a/src/features/ai-chat/types/index.ts b/src/features/ai-chat/types.ts similarity index 100% rename from src/features/ai-chat/types/index.ts rename to src/features/ai-chat/types.ts diff --git a/src/features/ai-chat/utils/message.ts b/src/features/ai-chat/utils.ts similarity index 100% rename from src/features/ai-chat/utils/message.ts rename to src/features/ai-chat/utils.ts diff --git a/src/features/ai-chat/utils/index.ts b/src/features/ai-chat/utils/index.ts deleted file mode 100644 index f5455874..00000000 --- a/src/features/ai-chat/utils/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './message'; diff --git a/src/features/ai-provider/stores/index.ts b/src/features/ai-provider/stores.ts similarity index 97% rename from src/features/ai-provider/stores/index.ts rename to src/features/ai-provider/stores.ts index ccd5842c..29cbbe2e 100644 --- a/src/features/ai-provider/stores/index.ts +++ b/src/features/ai-provider/stores.ts @@ -4,9 +4,9 @@ import { create } from 'zustand'; import { persist } from 'zustand/middleware'; import { NuwaIdentityKit } from '@/features/auth/services'; -import { createPersistConfig, db } from '@/storage'; -import { fetchAvailableModels } from '../services/models'; -import type { Model } from '../types'; +import { createPersistConfig, db } from '@/shared/storage'; +import { fetchAvailableModels } from './services/models'; +import type { Model } from './types'; export const AUTO_MODEL: Model = { id: 'openrouter/auto', diff --git a/src/features/ai-provider/types/index.ts b/src/features/ai-provider/types.ts similarity index 100% rename from src/features/ai-provider/types/index.ts rename to src/features/ai-provider/types.ts diff --git a/src/features/ai-provider/utils/index.ts b/src/features/ai-provider/utils.ts similarity index 98% rename from src/features/ai-provider/utils/index.ts rename to src/features/ai-provider/utils.ts index 7bbbf081..07448565 100644 --- a/src/features/ai-provider/utils/index.ts +++ b/src/features/ai-provider/utils.ts @@ -1,4 +1,4 @@ -import type { Model } from '../types'; +import type { Model } from './types'; export function isFreeModel(model: Model): boolean { const { pricing } = model; diff --git a/src/features/auth/hooks/use-auth.ts b/src/features/auth/hooks/use-auth.ts index 4416152b..a5a0958a 100644 --- a/src/features/auth/hooks/use-auth.ts +++ b/src/features/auth/hooks/use-auth.ts @@ -1,5 +1,3 @@ -'use client'; - import { type UseIdentityKitOptions, useIdentityKit, diff --git a/src/features/cap/stores/index.ts b/src/features/cap/stores/index.ts index 795c2805..5e01b140 100644 --- a/src/features/cap/stores/index.ts +++ b/src/features/cap/stores/index.ts @@ -3,7 +3,7 @@ import { create } from 'zustand'; import { persist } from 'zustand/middleware'; import { NuwaIdentityKit } from '@/features/auth/services'; -import { createPersistConfig, db } from '@/storage'; +import { createPersistConfig, db } from '@/shared/storage'; import type { InstalledCap, RemoteCap } from '../types'; // ================= Interfaces ================= // diff --git a/src/features/settings/stores/index.ts b/src/features/settings/stores.ts similarity index 98% rename from src/features/settings/stores/index.ts rename to src/features/settings/stores.ts index af583538..2a48e7e9 100644 --- a/src/features/settings/stores/index.ts +++ b/src/features/settings/stores.ts @@ -5,7 +5,7 @@ import { create } from 'zustand'; import { persist } from 'zustand/middleware'; import { NuwaIdentityKit } from '@/features/auth/services'; import type { Locale } from '@/shared/locales'; -import { createPersistConfig, db } from '@/storage'; +import { createPersistConfig, db } from '@/shared/storage'; // get current DID const getCurrentDID = async () => { diff --git a/src/storage/actions.ts b/src/shared/storage/actions.ts similarity index 100% rename from src/storage/actions.ts rename to src/shared/storage/actions.ts diff --git a/src/storage/db.ts b/src/shared/storage/db.ts similarity index 100% rename from src/storage/db.ts rename to src/shared/storage/db.ts diff --git a/src/storage/helper.ts b/src/shared/storage/helper.ts similarity index 100% rename from src/storage/helper.ts rename to src/shared/storage/helper.ts diff --git a/src/storage/index.ts b/src/shared/storage/index.ts similarity index 100% rename from src/storage/index.ts rename to src/shared/storage/index.ts diff --git a/src/storage/types.ts b/src/shared/storage/types.ts similarity index 100% rename from src/storage/types.ts rename to src/shared/storage/types.ts From 63ec5608bf2365831b6b1a0880da11229308d6c9 Mon Sep 17 00:00:00 2001 From: Mine77 Date: Thu, 24 Jul 2025 12:20:25 +0800 Subject: [PATCH 08/85] refactor: remove memory and files --- src/features/ai-chat/components/message.tsx | 16 - .../ai-chat/components/multimodal-input.tsx | 82 +---- .../ai-chat/components/preview-attachment.tsx | 44 --- src/features/ai-chat/hooks/index.ts | 2 - src/features/ai-chat/hooks/use-chat-page.ts | 8 +- .../ai-chat/hooks/use-chat-sessions.ts | 2 +- src/features/ai-chat/hooks/use-file.ts | 29 -- src/features/ai-chat/hooks/use-files.ts | 79 ----- .../ai-chat/hooks/use-update-chat-title.ts | 5 +- .../services/{handler/index.ts => handler.ts} | 8 +- src/features/ai-chat/services/index.ts | 2 - .../ai-chat/services/prompts/index.ts | 37 -- src/features/ai-chat/services/tools/index.ts | 18 - src/features/ai-chat/services/tools/memory.ts | 46 --- .../{stores/chat-store.ts => stores.ts} | 7 +- src/features/ai-chat/stores/file-store.ts | 322 ------------------ src/features/ai-chat/stores/index.ts | 5 - src/features/ai-chat/stores/memory-store.ts | 271 --------------- src/features/ai-chat/types.ts | 9 +- src/features/settings/components/index.ts | 1 - .../settings/components/memory-management.tsx | 311 ----------------- .../settings/components/settings-modal.tsx | 10 +- src/features/settings/hooks/use-memory.ts | 74 ---- src/features/settings/hooks/use-settings.ts | 2 - src/shared/hooks/use-storage.ts | 12 +- src/shared/storage/actions.ts | 6 - src/shared/storage/db.ts | 8 - 27 files changed, 20 insertions(+), 1396 deletions(-) delete mode 100644 src/features/ai-chat/components/preview-attachment.tsx delete mode 100644 src/features/ai-chat/hooks/use-file.ts delete mode 100644 src/features/ai-chat/hooks/use-files.ts rename src/features/ai-chat/services/{handler/index.ts => handler.ts} (90%) delete mode 100644 src/features/ai-chat/services/prompts/index.ts delete mode 100644 src/features/ai-chat/services/tools/index.ts delete mode 100644 src/features/ai-chat/services/tools/memory.ts rename src/features/ai-chat/{stores/chat-store.ts => stores.ts} (96%) delete mode 100644 src/features/ai-chat/stores/file-store.ts delete mode 100644 src/features/ai-chat/stores/index.ts delete mode 100644 src/features/ai-chat/stores/memory-store.ts delete mode 100644 src/features/settings/components/memory-management.tsx delete mode 100644 src/features/settings/hooks/use-memory.ts diff --git a/src/features/ai-chat/components/message.tsx b/src/features/ai-chat/components/message.tsx index 4c1f45de..1a7a12ac 100644 --- a/src/features/ai-chat/components/message.tsx +++ b/src/features/ai-chat/components/message.tsx @@ -11,7 +11,6 @@ import { MessageActions } from './message-actions'; import { MessageReasoning } from './message-reasoning'; import { MessageSource } from './message-source'; import { MessageText } from './message-text'; -import { PreviewAttachment } from './preview-attachment'; const PurePreviewMessage = ({ chatId, @@ -54,21 +53,6 @@ const PurePreviewMessage = ({ 'min-h-96': message.role === 'assistant' && requiresScrollPadding, })} > - {/* render attachments */} - {message.experimental_attachments && - message.experimental_attachments.length > 0 && ( -
- {message.experimental_attachments.map((attachment) => ( - - ))} -
- )} {/* render reasoning */} {message.parts?.map((part, index) => { diff --git a/src/features/ai-chat/components/multimodal-input.tsx b/src/features/ai-chat/components/multimodal-input.tsx index d4c90413..4958ca19 100644 --- a/src/features/ai-chat/components/multimodal-input.tsx +++ b/src/features/ai-chat/components/multimodal-input.tsx @@ -11,24 +11,21 @@ import { } from 'lucide-react'; import type React from 'react'; import { - type ChangeEvent, type Dispatch, memo, type SetStateAction, useCallback, useEffect, useRef, - useState, + useState } from 'react'; import { useLocalStorage, useWindowSize } from 'usehooks-ts'; -import { useFiles } from '@/features/ai-chat/hooks/use-files'; import { useScrollToBottom } from '@/features/ai-chat/hooks/use-scroll-to-bottom'; import { ModelSelector } from '@/features/ai-provider/components'; import { toast } from '@/shared/components'; import { Button } from '@/shared/components/ui/button'; import { useDevMode } from '@/shared/hooks/use-dev-mode'; -import { useLanguage } from '@/shared/hooks/use-language'; -import { PreviewAttachment } from './preview-attachment'; + import { SuggestedActions } from './suggested-actions'; function PureMultimodalInput({ @@ -37,8 +34,6 @@ function PureMultimodalInput({ setInput, status, stop, - attachments, - setAttachments, messages, setMessages, append, @@ -60,8 +55,6 @@ function PureMultimodalInput({ }) { const textareaRef = useRef(null); const { width } = useWindowSize(); - const { uploadFile } = useFiles(); - const { t } = useLanguage(); const isDevMode = useDevMode(); const [localStorageInput, setLocalStorageInput] = useLocalStorage( @@ -92,46 +85,15 @@ function PureMultimodalInput({ const [uploadQueue, setUploadQueue] = useState>([]); const submitForm = useCallback(() => { - handleSubmit(undefined, { - experimental_attachments: attachments, - }); + handleSubmit(undefined); - setAttachments([]); setLocalStorageInput(''); if (width && width > 768) { textareaRef.current?.focus(); } - }, [attachments, handleSubmit, setAttachments, setLocalStorageInput, width]); - - const handleFileChange = useCallback( - async (event: ChangeEvent) => { - const files = Array.from(event.target.files || []); - - setUploadQueue(files.map((file) => file.name)); + }, [handleSubmit, setLocalStorageInput, width]); - try { - const uploadPromises = files.map((file) => uploadFile(file)); - const uploadedAttachments = await Promise.all(uploadPromises); - const successfullyUploadedAttachments = uploadedAttachments.filter( - (attachment) => attachment !== undefined, - ); - - setAttachments((currentAttachments) => [ - ...currentAttachments, - ...successfullyUploadedAttachments, - ]); - } catch (error) { - toast({ - description: t('upload.errorUploading'), - type: 'error', - }); - } finally { - setUploadQueue([]); - } - }, - [setAttachments, uploadFile, t], - ); const { isAtBottom, scrollToBottom } = useScrollToBottom(); @@ -168,41 +130,9 @@ function PureMultimodalInput({ )} - {messages.length === 0 && - attachments.length === 0 && - uploadQueue.length === 0 && } + {messages.length === 0 && } - - - {(attachments.length > 0 || uploadQueue.length > 0) && ( -
- {attachments.map((attachment) => ( - - ))} - - {uploadQueue.map((filename) => ( - - ))} -
- )} +
{ - const { name, url, contentType } = attachment; - - return ( -
-
- {contentType ? ( - contentType.startsWith('image') ? ( - {name - ) : ( -
- ) - ) : ( -
- )} - - {isUploading && ( -
- -
- )} -
-
{name}
-
- ); -}; diff --git a/src/features/ai-chat/hooks/index.ts b/src/features/ai-chat/hooks/index.ts index a35660fb..618e1a38 100644 --- a/src/features/ai-chat/hooks/index.ts +++ b/src/features/ai-chat/hooks/index.ts @@ -2,8 +2,6 @@ export * from '../../settings/hooks/use-memory'; export * from './use-chat-default'; export * from './use-chat-page'; export * from './use-chat-sessions'; -export * from './use-file'; -export * from './use-files'; export * from './use-messages-ui'; export * from './use-scroll-to-bottom'; export * from './use-update-chat-title'; diff --git a/src/features/ai-chat/hooks/use-chat-page.ts b/src/features/ai-chat/hooks/use-chat-page.ts index c211c605..afd2a527 100644 --- a/src/features/ai-chat/hooks/use-chat-page.ts +++ b/src/features/ai-chat/hooks/use-chat-page.ts @@ -2,11 +2,9 @@ import { useEffect, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; -import { - type ChatSession, - createInitialChatSession, -} from '@/features/ai-chat/stores'; -import { convertToUIMessage } from '@/features/ai-chat/utils'; +import { createInitialChatSession } from '../stores'; +import type { ChatSession } from '../types'; +import { convertToUIMessage } from '../utils'; import { useChatSessions } from './use-chat-sessions'; // Specialized hook for chat page logic diff --git a/src/features/ai-chat/hooks/use-chat-sessions.ts b/src/features/ai-chat/hooks/use-chat-sessions.ts index 1b7f68ef..39935e5c 100644 --- a/src/features/ai-chat/hooks/use-chat-sessions.ts +++ b/src/features/ai-chat/hooks/use-chat-sessions.ts @@ -1,5 +1,5 @@ import { useCallback } from 'react'; -import { ChatStateStore } from '@/features/ai-chat/stores/chat-store'; +import { ChatStateStore } from '@/features/ai-chat/stores'; import type { ChatSession } from '@/features/ai-chat/types'; export const useChatSessions = () => { diff --git a/src/features/ai-chat/hooks/use-file.ts b/src/features/ai-chat/hooks/use-file.ts deleted file mode 100644 index 93c90c30..00000000 --- a/src/features/ai-chat/hooks/use-file.ts +++ /dev/null @@ -1,29 +0,0 @@ -'use client'; - -import { useCallback } from 'react'; -import { FileStateStore } from '@/features/ai-chat/stores'; - -// Individual file hook -export const useFile = (id: string) => { - const store = FileStateStore(); - const file = store.getFile(id); - - const deleteFile = useCallback(async () => { - await store.deleteFile(id); - }, [id]); - - const getFileURL = useCallback(async () => { - return await store.getFileURL(id); - }, [id]); - - const getFileBlob = useCallback(async () => { - return await store.getFileBlob(id); - }, [id]); - - return { - file, - deleteFile, - getFileURL, - getFileBlob, - }; -}; diff --git a/src/features/ai-chat/hooks/use-files.ts b/src/features/ai-chat/hooks/use-files.ts deleted file mode 100644 index 02a5d969..00000000 --- a/src/features/ai-chat/hooks/use-files.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { useCallback } from 'react'; -import { FileStateStore } from '@/features/ai-chat/stores'; -import { useLanguage } from '@/shared/hooks'; - -// File management hook -export const useFiles = () => { - const { t } = useLanguage(); - const store = FileStateStore(); - - const storeFile = useCallback(async (file: File) => { - return await store.storeFile(file); - }, []); - - const deleteFile = useCallback(async (id: string) => { - await store.deleteFile(id); - }, []); - - const getFileURL = useCallback(async (id: string) => { - return await store.getFileURL(id); - }, []); - - const getFileBlob = useCallback(async (id: string) => { - return await store.getFileBlob(id); - }, []); - - const clearAllFiles = useCallback(async () => { - await store.clearAllFiles(); - }, []); - - const validateFile = useCallback((file: File) => { - return store.validateFile(file); - }, []); - - const getFilesByType = useCallback((type: string) => { - return store.getFilesByType(type); - }, []); - - const uploadFile = useCallback( - async (file: File) => { - // validate file - const validation = validateFile(file); - if (!validation.valid) { - throw new Error(validation.error || 'Invalid file'); - } - - // store file - const storedFile = await storeFile(file); - - // get file url - const url = await getFileURL(storedFile.id); - - if (!url) { - throw new Error(t('upload.failedCreateUrl')); - } - - return { - url, - name: storedFile.name, - contentType: storedFile.type, - }; - }, - [validateFile, storeFile, getFileURL, t], - ); - - return { - files: store.getAllFiles(), - filesMap: store.files, - totalSize: store.getTotalSize(), - storeFile, - deleteFile, - getFileURL, - getFileBlob, - clearAllFiles, - validateFile, - getFilesByType, - getFile: store.getFile, - uploadFile, - }; -}; diff --git a/src/features/ai-chat/hooks/use-update-chat-title.ts b/src/features/ai-chat/hooks/use-update-chat-title.ts index c08117e9..818ad640 100644 --- a/src/features/ai-chat/hooks/use-update-chat-title.ts +++ b/src/features/ai-chat/hooks/use-update-chat-title.ts @@ -1,6 +1,7 @@ import { useCallback } from 'react'; -import { generateTitleFromUserMessage } from '@/features/ai-chat/services'; -import { type ChatSession, ChatStateStore } from '@/features/ai-chat/stores'; +import { generateTitleFromUserMessage } from '../services'; +import { ChatStateStore } from '../stores'; +import type { ChatSession } from '../types'; export const useUpdateChatTitle = (sessionId: string) => { const store = ChatStateStore(); diff --git a/src/features/ai-chat/services/handler/index.ts b/src/features/ai-chat/services/handler.ts similarity index 90% rename from src/features/ai-chat/services/handler/index.ts rename to src/features/ai-chat/services/handler.ts index 11b75243..ba92fa92 100644 --- a/src/features/ai-chat/services/handler/index.ts +++ b/src/features/ai-chat/services/handler.ts @@ -5,13 +5,11 @@ import { smoothStream, streamText, } from 'ai'; -import { ChatStateStore } from '@/features/ai-chat/stores/chat-store'; +import { ChatStateStore } from '@/features/ai-chat/stores'; import { llmProvider } from '@/features/ai-provider/services'; import { CapStateStore } from '@/features/cap/stores'; import { SettingsStateStore } from '@/features/settings/stores'; import { generateUUID } from '@/shared/utils'; -import { devModeSystemPrompt, systemPrompt } from '../prompts'; -import { tools } from '../tools'; // Error handling function function errorHandler(error: unknown) { @@ -68,7 +66,7 @@ const handleAIRequest = async ({ const { currentCap } = CapStateStore.getState(); - const prompt = isDevMode ? (currentCap? currentCap.prompt: devModeSystemPrompt()) : systemPrompt(); + const prompt = 'You are a friendly assistant! Keep your responses concise and helpful.'; const result = streamText({ model: llmProvider.chat(), @@ -77,7 +75,7 @@ const handleAIRequest = async ({ maxSteps: 5, experimental_transform: smoothStream({ chunking: 'word' }), experimental_generateMessageId: generateUUID, - tools, + tools: {}, abortSignal: signal, async onFinish({ response, reasoning, sources }) { const finalMessages = appendResponseMessages({ diff --git a/src/features/ai-chat/services/index.ts b/src/features/ai-chat/services/index.ts index 09be3c90..ed3463f4 100644 --- a/src/features/ai-chat/services/index.ts +++ b/src/features/ai-chat/services/index.ts @@ -1,5 +1,3 @@ -'use client'; - import { ChatSDKError } from '@/features/ai-chat/errors/chatsdk-errors'; import { handleAIRequest } from './handler'; diff --git a/src/features/ai-chat/services/prompts/index.ts b/src/features/ai-chat/services/prompts/index.ts deleted file mode 100644 index 3963ee39..00000000 --- a/src/features/ai-chat/services/prompts/index.ts +++ /dev/null @@ -1,37 +0,0 @@ - -const memoryPrompt = ` -## Memory Management -You have memory management capabilities. You need to determine when to use memory functionality based on the following principles: - -### Memory Storage Triggers: -1. **Important Personal Information**: User mentions names, preferences, goals, important dates, etc. -2. **Continuous Tasks**: Cross-session projects, learning plans, long-term goals -3. **Key Decisions**: User's important choices or decisions -4. **Repeated Topics**: User discusses topics or interests multiple times -5. **Error Corrections**: User corrects your understanding or provides clarification - -### Memory Retrieval Triggers: -1. **Personalized Needs**: Need to provide suggestions based on user's historical preferences -2. **Context Continuity**: Current conversation involves topics discussed previously -3. **Task Continuation**: Continue tasks that were not completed previously -4. **Avoid Repeated Questions**: Prevent asking known information - -### Situations When Memory is Not Needed: -- General questions and factual queries -- Temporary, one-time information -- Too specific details - -During each interaction, first evaluate if memory retrieval is needed, and then evaluate if new information should be stored at the end of the conversation. -Use the memory management silently. Do not tell the user that you are using memory management. -` - -export const regularPrompt = - 'You are a friendly assistant! Keep your responses concise and helpful.'; - -export const systemPrompt = () => { - return `${regularPrompt}`; -}; - -export const devModeSystemPrompt = () => { - return `${regularPrompt}\n\n${memoryPrompt}`; -}; diff --git a/src/features/ai-chat/services/tools/index.ts b/src/features/ai-chat/services/tools/index.ts deleted file mode 100644 index d9d918dc..00000000 --- a/src/features/ai-chat/services/tools/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { ModelStateStore } from '@/features/ai-provider/stores'; -import { SettingsStateStore } from '@/features/settings/stores'; -import { queryMemory, saveMemory } from '../tools/memory'; - -const selectedModel = ModelStateStore.getState().selectedModel; -const isDevMode = SettingsStateStore.getState().settings.devMode; - -const userModeTools = { -}; - -const devModeTools = { - saveMemory: saveMemory(), - queryMemory: queryMemory(), -}; - -export const modelSupportTools = - selectedModel.supported_parameters.includes('tools'); -export const tools = isDevMode ? devModeTools : userModeTools; diff --git a/src/features/ai-chat/services/tools/memory.ts b/src/features/ai-chat/services/tools/memory.ts deleted file mode 100644 index 8f142d5c..00000000 --- a/src/features/ai-chat/services/tools/memory.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { tool } from 'ai'; -import { z } from 'zod'; -import { MemoryStateStore } from '../../stores/memory-store'; - -export const saveMemory = () => - tool({ - description: - 'Store a memory for later use. This can be used to remember important information or context. Alwasy use English for saving and retrieving memories.', - parameters: z.object({ - memory: z.string().describe('the memory text to save'), - reason: z.string().describe('the reason for saving the memory'), - }), - execute: async ({ memory,reason }) => { - await MemoryStateStore.getState().saveMemory(memory); - return { - memory: memory, - reason: reason, - }; - }, - }); - -export const queryMemory = () => - tool({ - description: - 'Retrieve one or more memory about the user by providing a query and an amount. Alwasy use English for saving and retrieving memories.', - parameters: z.object({ - query: z.string().describe('the query to search for in the memory'), - amount: z - .number() - .default(1) - .describe('the number of memories to retrieve'), - reason: z.string().describe('the reason for retrieving the memory'), - }), - execute: async ({ query, amount,reason }) => { - const memories = await MemoryStateStore.getState().queryMemories(query, { - limit: amount, - }); - return { - memories: memories.map((m) => ({ - text: m.text, - createdAt: m.createdAt, - })), - reason: reason, - }; - }, - }); diff --git a/src/features/ai-chat/stores/chat-store.ts b/src/features/ai-chat/stores.ts similarity index 96% rename from src/features/ai-chat/stores/chat-store.ts rename to src/features/ai-chat/stores.ts index cb747184..5fafae1b 100644 --- a/src/features/ai-chat/stores/chat-store.ts +++ b/src/features/ai-chat/stores.ts @@ -7,7 +7,7 @@ import { persist } from 'zustand/middleware'; import { NuwaIdentityKit } from '@/features/auth/services'; import { createPersistConfig, db } from '@/shared/storage'; import { generateUUID } from '@/shared/utils'; -import type { ChatSession } from '../types'; +import type { ChatSession } from './types'; // ================= Constants ================= // export const createInitialChatSession = (): ChatSession => ({ @@ -145,10 +145,6 @@ export const ChatStateStore = create()( .where(['did', 'id']) .equals([currentDID, id]) .delete(); - await chatDB.streams - .where(['did', 'chatId']) - .equals([currentDID, id]) - .delete(); } catch (error) { console.error('Failed to delete from DB:', error); } @@ -215,7 +211,6 @@ export const ChatStateStore = create()( const clearDB = async () => { try { await chatDB.chats.clear(); - await chatDB.streams.clear(); } catch (error) { console.error('Failed to clear DB:', error); } diff --git a/src/features/ai-chat/stores/file-store.ts b/src/features/ai-chat/stores/file-store.ts deleted file mode 100644 index b140375b..00000000 --- a/src/features/ai-chat/stores/file-store.ts +++ /dev/null @@ -1,322 +0,0 @@ -// file-store.ts -// Client-side file storage system using unified storage architecture -import { create } from 'zustand'; -import { persist } from 'zustand/middleware'; -import { NuwaIdentityKit } from '@/features/auth/services'; -import { createPersistConfig, db } from '@/shared/storage'; -import { generateUUID } from '@/shared/utils'; - -// ================= Constants & Types ================= // - -// Supported file types -export const SUPPORTED_FILE_TYPES = { - IMAGE: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'], - DOCUMENT: ['application/pdf', 'text/plain', 'application/msword'], - ALL: [ - 'image/jpeg', - 'image/png', - 'image/gif', - 'image/webp', - 'application/pdf', - 'text/plain', - ], -} as const; - -// Maximum file size (10MB) -export const MAX_FILE_SIZE = 10 * 1024 * 1024; - -// get current DID -const getCurrentDID = async () => { - const { getDid } = await NuwaIdentityKit(); - return await getDid(); -}; - -// ================= Interfaces ================= // - -// File metadata interface -export interface StoredFile { - id: string; - name: string; - type: string; - size: number; - uploadedAt: number; -} - -// File data interface (including actual Blob data) -export interface FileData { - id: string; - blob: Blob; -} - -// File validation result interface -interface ValidationResult { - valid: boolean; - error?: string; -} - -// File store state interface -interface FileStoreState { - files: Record; - - // File validation - validateFile: (file: File) => ValidationResult; - - // File management - storeFile: (file: File) => Promise; - getFile: (id: string) => StoredFile | null; - getFileBlob: (id: string) => Promise; - getFileURL: (id: string) => Promise; - deleteFile: (id: string) => Promise; - - // File queries - getAllFiles: () => StoredFile[]; - getFilesByType: (type: string) => StoredFile[]; - getTotalSize: () => number; - - // Cleanup operations - clearAllFiles: () => Promise; - - // Data persistence - loadFromDB: () => Promise; - saveToDB: () => Promise; -} - -// ================= Database Reference ================= // - -const fileDB = db; - -// ================= Persist Configuration ================= // - -const persistConfig = createPersistConfig({ - name: 'file-storage', - getCurrentDID: getCurrentDID, - partialize: (state) => ({ - files: state.files, - }), - onRehydrateStorage: () => (state?: FileStoreState) => { - if (state) { - state.loadFromDB(); - } - }, -}); - -// ================= Store Definition ================= // - -export const FileStateStore = create()( - persist( - (set, get) => ({ - // Store state - files: {}, - - // File validation - validateFile: (file: File): ValidationResult => { - // Check file size - if (file.size > MAX_FILE_SIZE) { - return { - valid: false, - error: `File size should be less than ${MAX_FILE_SIZE / 1024 / 1024}MB`, - }; - } - - // Check file type - if (!SUPPORTED_FILE_TYPES.ALL.includes(file.type as any)) { - return { - valid: false, - error: `File type ${file.type} is not supported`, - }; - } - - return { valid: true }; - }, - - // File upload and management - storeFile: async (file: File): Promise => { - const validation = get().validateFile(file); - if (!validation.valid) { - throw new Error(validation.error); - } - - const id = generateUUID(); - const storedFile: StoredFile = { - id, - name: file.name, - type: file.type, - size: file.size, - uploadedAt: Date.now(), - }; - - try { - // Save file data to IndexedDB - await fileDB.fileData.add({ - id, - blob: file, - }); - - // Save metadata to state - set((state) => ({ - files: { - ...state.files, - [id]: storedFile, - }, - })); - - // Save metadata to IndexedDB asynchronously - get().saveToDB(); - - return storedFile; - } catch (error) { - console.error('Failed to upload file:', error); - throw new Error('Failed to save file'); - } - }, - - // File retrieval methods - getFile: (id: string): StoredFile | null => { - const { files } = get(); - return files[id] || null; - }, - - getFileBlob: async (id: string): Promise => { - try { - const fileData = await fileDB.fileData.get(id); - return fileData?.blob || null; - } catch (error) { - console.error('Failed to get file blob:', error); - return null; - } - }, - - getFileURL: async (id: string): Promise => { - try { - const blob = await get().getFileBlob(id); - if (blob) { - return URL.createObjectURL(blob); - } - return null; - } catch (error) { - console.error('Failed to create file URL:', error); - return null; - } - }, - - // File deletion - deleteFile: async (id: string): Promise => { - try { - // Delete file data from IndexedDB - await fileDB.fileData.delete(id); - await fileDB.files.delete(id); - - // Delete metadata from state - set((state) => { - const { [id]: deleted, ...restFiles } = state.files; - return { files: restFiles }; - }); - } catch (error) { - console.error('Failed to delete file:', error); - throw new Error('Failed to delete file'); - } - }, - - // File listing and filtering - getAllFiles: (): StoredFile[] => { - const { files } = get(); - return Object.values(files).sort((a, b) => b.uploadedAt - a.uploadedAt); - }, - - getFilesByType: (type: string): StoredFile[] => { - const { files } = get(); - return Object.values(files) - .filter((file) => file.type.startsWith(type)) - .sort((a, b) => b.uploadedAt - a.uploadedAt); - }, - - // File queries - getTotalSize: (): number => { - const { files } = get(); - return Object.values(files).reduce( - (total, file) => total + file.size, - 0, - ); - }, - - // Cleanup operations - clearAllFiles: async (): Promise => { - try { - await fileDB.fileData.clear(); - await fileDB.files.clear(); - set({ files: {} }); - } catch (error) { - console.error('Failed to clear files:', error); - throw new Error('Failed to clear files'); - } - }, - - // Data persistence - loadFromDB: async () => { - if (typeof window === 'undefined') return; - - try { - const currentDID = await getCurrentDID(); - if (!currentDID) return; - - const files = await fileDB.files - .where('did') - .equals(currentDID) - .toArray(); - - const filesMap: Record = {}; - files.forEach((file: StoredFile) => { - filesMap[file.id] = file; - }); - - set((state) => ({ - files: { ...state.files, ...filesMap }, - })); - } catch (error) { - console.error('Failed to load from DB:', error); - } - }, - - saveToDB: async () => { - if (typeof window === 'undefined') return; - - try { - const { files } = get(); - const filesToSave = Object.values(files); - - // Use bulkPut to efficiently update data - await fileDB.files.bulkPut(filesToSave); - } catch (error) { - console.error('Failed to save to DB:', error); - } - }, - }), - persistConfig, - ), -); - -// ================= Utility Functions ================= // - -// 工具函数:格式化文件大小 -export function formatFileSize(bytes: number): string { - if (bytes === 0) return '0 Bytes'; - - const k = 1024; - const sizes = ['Bytes', 'KB', 'MB', 'GB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - - return `${Number.parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`; -} - -// 工具函数:检查是否为图片文件 -export function isImageFile(file: StoredFile): boolean { - return SUPPORTED_FILE_TYPES.IMAGE.includes(file.type as any); -} - -// 工具函数:获取文件图标 -export function getFileIcon(file: StoredFile): string { - if (isImageFile(file)) return '🖼️'; - if (file.type === 'application/pdf') return '📄'; - if (file.type.startsWith('text/')) return '📝'; - return '📎'; -} diff --git a/src/features/ai-chat/stores/index.ts b/src/features/ai-chat/stores/index.ts deleted file mode 100644 index a3722c08..00000000 --- a/src/features/ai-chat/stores/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -// Re-export types for convenience -export type { ChatSession, StreamRecord } from '../types'; -export * from './chat-store'; -export * from './file-store'; -export * from './memory-store'; diff --git a/src/features/ai-chat/stores/memory-store.ts b/src/features/ai-chat/stores/memory-store.ts deleted file mode 100644 index a8bc4031..00000000 --- a/src/features/ai-chat/stores/memory-store.ts +++ /dev/null @@ -1,271 +0,0 @@ -// memory-store.ts -// Store for managing memories with unified storage and vector search - -import { - env, - type FeatureExtractionPipeline, - pipeline, -} from '@xenova/transformers'; -import { create } from 'zustand'; -import { persist } from 'zustand/middleware'; -import { NuwaIdentityKit } from '@/features/auth/services'; -import { createPersistConfig, db } from '@/shared/storage'; -import { generateUUID } from '@/shared/utils'; - -env.allowLocalModels = false; - -// ================= Types ================= // - -interface Memory { - id: string; - text: string; - vector: number[]; - createdAt: number; - updatedAt: number; - metadata?: Record; -} - -interface QueryMemoryResult extends Memory { - similarity: number; -} - -interface QueryOptions { - limit?: number; -} - -// ================= Constants ================= // - -const defaultModel = 'Xenova/all-MiniLM-L6-v2'; -const pipePromise: Promise = pipeline( - 'feature-extraction', - defaultModel, -); - -// ================= Helper Functions ================= // - -// get current DID -const getCurrentDID = async () => { - const { getDid } = await NuwaIdentityKit(); - return await getDid(); -}; - -// Cosine similarity function -const cosineSimilarity = (vecA: number[], vecB: number[]): number => { - const dotProduct = vecA.reduce( - (sum, val, index) => sum + val * vecB[index], - 0, - ); - const magnitudeA = Math.sqrt(vecA.reduce((sum, val) => sum + val * val, 0)); - const magnitudeB = Math.sqrt(vecB.reduce((sum, val) => sum + val * val, 0)); - return dotProduct / (magnitudeA * magnitudeB); -}; - -// Function to get embeddings from text using HuggingFace pipeline -const getEmbeddingFromText = async (text: string): Promise => { - const pipe = await pipePromise; - const output = await pipe(text, { - pooling: 'mean', - normalize: true, - }); - return Array.from(output.data); -}; - -// ================= Database Reference ================= // - -const memoryDB = db; - -// ================= Store Interface ================= // - -interface MemoryStoreState { - memories: Record; - - // memory management - saveMemory: (text: string, metadata?: Record) => Promise; - queryMemories: ( - queryText: string, - options?: QueryOptions, - ) => Promise; - - // utility methods - clearAllMemories: () => Promise; - getMemoryCount: () => number; - deleteMemory: (id: string) => Promise; - - // data persistence - loadFromDB: () => Promise; - saveToDB: () => Promise; -} - -// ================= Persist Configuration ================= // - -const persistConfig = createPersistConfig({ - name: 'memory-storage', - getCurrentDID: getCurrentDID, - partialize: (state) => ({ - memories: state.memories, - }), - onRehydrateStorage: () => (state) => { - if (state) { - state.loadFromDB(); - } - }, -}); - -// ================= Store Factory ================= // - -export const MemoryStateStore = create()( - persist( - (set, get) => ({ - memories: {}, - - saveMemory: async (text: string, metadata?: Record) => { - try { - // Generate embedding for the text - const vector = await getEmbeddingFromText(text); - - const memory: Memory = { - id: generateUUID(), - text, - vector, - createdAt: Date.now(), - updatedAt: Date.now(), - metadata, - }; - - // Add to store - set((state) => ({ - memories: { - ...state.memories, - [memory.id]: memory, - }, - })); - - // Save to database - await get().saveToDB(); - - return memory.id; - } catch (error) { - console.error('Failed to save memory:', error); - throw error; - } - }, - - deleteMemory: async (id: string) => { - // Remove from store - set((state) => { - const { [id]: deleted, ...restMemories } = state.memories; - return { - memories: restMemories, - }; - }); - - // Delete from database - try { - const currentDID = await getCurrentDID(); - if (!currentDID) return; - - // 使用单个索引查询并过滤结果 - await memoryDB.memories - .where('did') - .equals(currentDID) - .and((item) => item.id === id) - .delete(); - } catch (error) { - console.error('Failed to delete memory from DB:', error); - } - }, - - queryMemories: async ( - queryText: string, - { limit = 10 }: QueryOptions = {}, - ) => { - try { - // Get embedding for the query text - const queryVector = await getEmbeddingFromText(queryText); - - const { memories } = get(); - const memoryList = Object.values(memories); - - // Calculate cosine similarity for each memory and sort by similarity - const similarities: QueryMemoryResult[] = memoryList.map((memory) => { - const similarity = cosineSimilarity(queryVector, memory.vector); - return { ...memory, similarity }; - }); - - // Sort by similarity (descending) and return top results - similarities.sort((a, b) => b.similarity - a.similarity); - return similarities.slice(0, limit); - } catch (error) { - console.error('Failed to query memories:', error); - throw error; - } - }, - - clearAllMemories: async () => { - set({ - memories: {}, - }); - - // Clear database - try { - const currentDID = await getCurrentDID(); - if (!currentDID) return; - - await memoryDB.memories.where('did').equals(currentDID).delete(); - } catch (error) { - console.error('Failed to clear memories from DB:', error); - } - }, - - getMemoryCount: () => { - const { memories } = get(); - return Object.keys(memories).length; - }, - - loadFromDB: async () => { - if (typeof window === 'undefined') return; - - try { - const currentDID = await getCurrentDID(); - if (!currentDID) return; - - const memories = await memoryDB.memories - .where('did') - .equals(currentDID) - .toArray(); - - // Sort by updatedAt in descending order - const sortedMemories = memories.sort( - (a: Memory, b: Memory) => b.updatedAt - a.updatedAt, - ); - - const memoriesMap: Record = {}; - sortedMemories.forEach((memory: Memory) => { - memoriesMap[memory.id] = memory; - }); - - set((state) => ({ - memories: { ...state.memories, ...memoriesMap }, - })); - } catch (error) { - console.error('Failed to load memories from DB:', error); - } - }, - - saveToDB: async () => { - if (typeof window === 'undefined') return; - - try { - const { memories } = get(); - const memoriesToSave = Object.values(memories); - - // Use bulkPut to efficiently update data - await memoryDB.memories.bulkPut(memoriesToSave); - } catch (error) { - console.error('Failed to save memories to DB:', error); - } - }, - }), - persistConfig, - ), -); diff --git a/src/features/ai-chat/types.ts b/src/features/ai-chat/types.ts index e9d79ab2..b96cd212 100644 --- a/src/features/ai-chat/types.ts +++ b/src/features/ai-chat/types.ts @@ -9,11 +9,4 @@ export interface ChatSession { updatedAt: number; messages: Message[]; cap: InstalledCap | null; -} - -// stream ID management interface -export interface StreamRecord { - id: string; - chatId: string; - createdAt: number; -} +} \ No newline at end of file diff --git a/src/features/settings/components/index.ts b/src/features/settings/components/index.ts index 7fa47087..1739098f 100644 --- a/src/features/settings/components/index.ts +++ b/src/features/settings/components/index.ts @@ -1,2 +1 @@ -export { MemoryManagement } from './memory-management'; export { SettingsModal } from './settings-modal'; diff --git a/src/features/settings/components/memory-management.tsx b/src/features/settings/components/memory-management.tsx deleted file mode 100644 index a6247137..00000000 --- a/src/features/settings/components/memory-management.tsx +++ /dev/null @@ -1,311 +0,0 @@ -'use client'; - -import { Edit2, Plus, Trash2 } from 'lucide-react'; -import { useState } from 'react'; -import { useMemory } from '@/features/settings/hooks/use-memory'; -import { toast } from '@/shared/components'; -import { - Button, - Card, - CardDescription, - CardHeader, - CardTitle, - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - ScrollArea, - Textarea, -} from '@/shared/components/ui'; -import { formatRelativeTime } from '@/shared/utils'; - -export function MemoryManagement() { - const { - memories, - deleteMemory, - clearAllMemories, - getMemoryCount, - isLoading, - addMemory, - editMemory, - } = useMemory(); - const [showConfirm, setShowConfirm] = useState(false); - const [showAddDialog, setShowAddDialog] = useState(false); - const [showEditDialog, setShowEditDialog] = useState(false); - const [newMemoryText, setNewMemoryText] = useState(''); - const [editMemoryText, setEditMemoryText] = useState(''); - const [editingMemory, setEditingMemory] = useState<{ - id: string; - createdAt: number; - } | null>(null); - const [deletingId, setDeletingId] = useState(null); - const [isAdding, setIsAdding] = useState(false); - const [isEditing, setIsEditing] = useState(false); - - const handleDeleteMemory = async (id: string) => { - setDeletingId(id); - try { - await deleteMemory(id); - toast({ - type: 'success', - description: 'Memory deleted successfully', - }); - } catch (error) { - toast({ - type: 'error', - description: 'Failed to delete memory', - }); - } finally { - setDeletingId(null); - } - }; - - const handleClearAll = async () => { - try { - await clearAllMemories(); - toast({ - type: 'success', - description: 'All memories cleared successfully', - }); - } catch (error) { - toast({ - type: 'error', - description: 'Failed to clear memories', - }); - } finally { - setShowConfirm(false); - } - }; - - const handleAddMemory = async () => { - if (!newMemoryText.trim()) return; - - setIsAdding(true); - try { - await addMemory(newMemoryText.trim()); - toast({ - type: 'success', - description: 'Memory added successfully', - }); - setNewMemoryText(''); - setShowAddDialog(false); - } catch (error) { - toast({ - type: 'error', - description: 'Failed to add memory', - }); - } finally { - setIsAdding(false); - } - }; - - const handleOpenEditDialog = (memory: { - id: string; - text: string; - createdAt: number; - }) => { - setEditingMemory({ id: memory.id, createdAt: memory.createdAt }); - setEditMemoryText(memory.text); - setShowEditDialog(true); - }; - - const handleEditMemory = async () => { - if (!editingMemory || !editMemoryText.trim()) return; - - setIsEditing(true); - try { - await editMemory( - editingMemory.id, - editMemoryText.trim(), - editingMemory.createdAt, - ); - toast({ - type: 'success', - description: 'Memory updated successfully', - }); - setEditMemoryText(''); - setEditingMemory(null); - setShowEditDialog(false); - } catch (error) { - toast({ - type: 'error', - description: 'Failed to update memory', - }); - } finally { - setIsEditing(false); - } - }; - - const memoryCount = getMemoryCount(); - - return ( -
-
-
-

- {memoryCount} {memoryCount === 1 ? 'memory' : 'memories'} stored -

-
-
- -
-
- - {memories.length === 0 ? ( - - - - No memories stored - - - Memories will appear here when the AI saves information about your - conversations - - - - ) : ( - -
- {memories.map((memory) => ( - -
-
-

- {formatRelativeTime(memory.createdAt)} -

-

- {memory.text} -

-
-
- - -
-
-
- ))} -
-
- )} - - {/* Confirmation Dialog */} - {showConfirm && ( -
-
-
Clear All Memories?
-
- This will permanently delete all stored memories. This action - cannot be undone. -
-
- - -
-
-
- )} - - {/* Add Memory Dialog */} - - - - Add New Memory - -
-