From 1561c054b7530808468f194b2948cfeb225d45c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BB=8E=E4=BD=95=E5=BC=80=E5=A7=8B123?= <64304674+yeahhe365@users.noreply.github.com> Date: Thu, 8 Jan 2026 11:56:00 +0800 Subject: [PATCH] 1 --- .DS_Store | Bin 0 -> 6148 bytes prisma/App.tsx | 178 ++---------- prisma/ExpertCard.tsx | 13 +- prisma/SettingsModal.tsx | 148 +++------- prisma/components/ChatArea.tsx | 16 +- prisma/components/Header.tsx | 18 +- prisma/components/InputSection.tsx | 10 +- prisma/components/Logo.tsx | 105 ++++++++ prisma/components/MarkdownRenderer.tsx | 25 +- prisma/components/settings/ApiSection.tsx | 62 +++++ prisma/components/settings/GithubSection.tsx | 50 ++++ prisma/components/settings/LevelSelect.tsx | 42 +++ .../components/settings/ThinkingSection.tsx | 69 +++++ prisma/config.ts | 21 +- prisma/hooks/useAppLogic.ts | 228 ++++++++++++++++ prisma/hooks/useDeepThink.ts | 253 ++++++++---------- prisma/hooks/useDeepThinkState.ts | 73 +++++ prisma/index.html | 3 + prisma/interceptor.ts | 115 +++++++- prisma/metadata.json | 5 +- prisma/package.json | 2 + prisma/services/deepThink/manager.ts | 67 ++++- prisma/services/deepThink/prompts.ts | 40 ++- prisma/types.ts | 11 +- 24 files changed, 1105 insertions(+), 449 deletions(-) create mode 100644 .DS_Store create mode 100644 prisma/components/Logo.tsx create mode 100644 prisma/components/settings/ApiSection.tsx create mode 100644 prisma/components/settings/GithubSection.tsx create mode 100644 prisma/components/settings/LevelSelect.tsx create mode 100644 prisma/components/settings/ThinkingSection.tsx create mode 100644 prisma/hooks/useAppLogic.ts create mode 100644 prisma/hooks/useDeepThinkState.ts diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..6a543b81ec143b348112fdb7531faa4591e35b20 GIT binary patch literal 6148 zcmeHK%Sr=55Uh?tqh50KIA0L(3moI-5YPzb2TaUGh$Jdr1Yy6@-_h#nj_l$dJc&p* z%v8^G*UW6e&I5pqUsk8U5Ws*%QPk-$?d}ZqJh@MlGR6#ZOs4D2-E!SA&|mD*wIART zQ(Rzz#`-r)MpTv=Oa3TE57@^RPq@c39BfcqMP!=Jc+~d2DbF zal@6Iqrw$]Bt2HweNu87UBP#_ct1ww&P;KvH^%vKv58%7-pgaV@`QGT|5`#e2s6U!}& rn&#E&(CD`=0c_|#a#Dg8PwF!-JJyCeiuM;eF)ji{kf=g|Ur^u^Q{+J3 literal 0 HcmV?d00001 diff --git a/prisma/App.tsx b/prisma/App.tsx index 21190a3..a96fb31 100644 --- a/prisma/App.tsx +++ b/prisma/App.tsx @@ -1,8 +1,6 @@ -import React, { useState, useEffect } from 'react'; -import { ModelOption, AppConfig, ChatMessage } from './types'; -import { getValidThinkingLevels } from './config'; -import { useDeepThink } from './hooks/useDeepThink'; -import { useChatSessions } from './hooks/useChatSessions'; + +import React from 'react'; +import { useAppLogic } from './hooks/useAppLogic'; import SettingsModal from './SettingsModal'; import Header from './components/Header'; @@ -11,149 +9,32 @@ import Sidebar from './components/Sidebar'; import ChatArea from './components/ChatArea'; const App = () => { - // Session Management - const { - sessions, - currentSessionId, - setCurrentSessionId, - createSession, - updateSessionMessages, - deleteSession, - getSession - } = useChatSessions(); - - // UI State - const [isSidebarOpen, setIsSidebarOpen] = useState(true); - const [isSettingsOpen, setIsSettingsOpen] = useState(false); - - // Active Chat State - const [messages, setMessages] = useState([]); - const [query, setQuery] = useState(''); - - // App Configuration - const [selectedModel, setSelectedModel] = useState('gemini-3-flash-preview'); - const [config, setConfig] = useState({ - planningLevel: 'high', - expertLevel: 'high', - synthesisLevel: 'high', - customApiKey: '', - customBaseUrl: '', - enableCustomApi: false - }); - - // Deep Think Engine - const { - appState, - managerAnalysis, - experts, - finalOutput, - synthesisThoughts, - runDynamicDeepThink, - stopDeepThink, - resetDeepThink, + const { + sessions, + currentSessionId, + messages, + query, + setQuery, + selectedModel, + setSelectedModel, + config, + setConfig, + isSidebarOpen, + setIsSidebarOpen, + isSettingsOpen, + setIsSettingsOpen, + appState, + managerAnalysis, + experts, + finalOutput, processStartTime, - processEndTime - } = useDeepThink(); - - // Handle Model Constraints - useEffect(() => { - const validLevels = getValidThinkingLevels(selectedModel); - setConfig(prev => ({ - ...prev, - planningLevel: validLevels.includes(prev.planningLevel) ? prev.planningLevel : 'low', - expertLevel: validLevels.includes(prev.expertLevel) ? prev.expertLevel : 'low', - synthesisLevel: validLevels.includes(prev.synthesisLevel) ? prev.synthesisLevel : 'high', - })); - }, [selectedModel]); - - // Sync Messages when switching sessions - useEffect(() => { - if (currentSessionId) { - const session = getSession(currentSessionId); - if (session) { - setMessages(session.messages); - setSelectedModel(session.model || 'gemini-3-flash-preview'); - } - } else { - setMessages([]); - } - }, [currentSessionId, getSession]); - - // Handle AI Completion - useEffect(() => { - if (appState === 'completed') { - const finalizedMessage: ChatMessage = { - id: `ai-${Date.now()}`, - role: 'model', - content: finalOutput, - analysis: managerAnalysis, - experts: experts, - synthesisThoughts: synthesisThoughts, - isThinking: false, - totalDuration: (processStartTime && processEndTime) ? (processEndTime - processStartTime) : undefined - }; - - const newMessages = [...messages, finalizedMessage]; - setMessages(newMessages); - - if (currentSessionId) { - updateSessionMessages(currentSessionId, newMessages); - } else { - createSession(newMessages, selectedModel); - } - - resetDeepThink(); - } - }, [appState, finalOutput, managerAnalysis, experts, synthesisThoughts, resetDeepThink, processStartTime, processEndTime, currentSessionId, messages, selectedModel, createSession, updateSessionMessages]); - - const handleRun = () => { - if (!query.trim()) return; - - const userMsg: ChatMessage = { - id: `user-${Date.now()}`, - role: 'user', - content: query - }; - - const newMessages = [...messages, userMsg]; - setMessages(newMessages); // Optimistic update - - // Manage Session Persistence - let activeSessionId = currentSessionId; - if (!activeSessionId) { - activeSessionId = createSession(newMessages, selectedModel); - } else { - updateSessionMessages(activeSessionId, newMessages); - } - - // Run AI - runDynamicDeepThink(query, messages, selectedModel, config); - setQuery(''); - }; - - const handleNewChat = () => { - stopDeepThink(); - setCurrentSessionId(null); - setMessages([]); - setQuery(''); - resetDeepThink(); - if (window.innerWidth < 1024) setIsSidebarOpen(false); - }; - - const handleSelectSession = (id: string) => { - stopDeepThink(); - resetDeepThink(); - setCurrentSessionId(id); - if (window.innerWidth < 1024) setIsSidebarOpen(false); - }; - - const handleDeleteSession = (id: string, e: React.MouseEvent) => { - e.stopPropagation(); - deleteSession(id); - if (currentSessionId === id) { - handleNewChat(); - } - }; + processEndTime, + handleRun, + handleNewChat, + handleSelectSession, + handleDeleteSession, + stopDeepThink + } = useAppLogic(); return (
@@ -196,7 +77,6 @@ const App = () => { processEndTime={processEndTime} /> - {/* Floating Footer Input */}
{ ); }; -export default App; \ No newline at end of file +export default App; diff --git a/prisma/ExpertCard.tsx b/prisma/ExpertCard.tsx index a4c68bb..26450e7 100644 --- a/prisma/ExpertCard.tsx +++ b/prisma/ExpertCard.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { Bot, Loader2, CheckCircle2, X, BrainCircuit, MessageSquareText, Thermometer, Timer } from 'lucide-react'; +import { Bot, Loader2, CheckCircle2, X, BrainCircuit, MessageSquareText, Thermometer, Timer, Repeat } from 'lucide-react'; import MarkdownRenderer from './components/MarkdownRenderer'; import { ExpertResult } from './types'; @@ -47,6 +47,7 @@ const ExpertCard = ({ expert }: { expert: ExpertResult }) => { const isDone = expert.status === 'completed'; const isPending = expert.status === 'pending'; const isError = expert.status === 'error'; + const round = expert.round || 1; // Auto-switch to thoughts if that's all we have so far React.useEffect(() => { @@ -72,7 +73,15 @@ const ExpertCard = ({ expert }: { expert: ExpertResult }) => {
-

{expert.role}

+
+

{expert.role}

+ {round > 1 && ( +
+ + Round {round} +
+ )} +
{/* Timer for Expert */} diff --git a/prisma/SettingsModal.tsx b/prisma/SettingsModal.tsx index c50383c..f81ee92 100644 --- a/prisma/SettingsModal.tsx +++ b/prisma/SettingsModal.tsx @@ -1,7 +1,18 @@ + import React from 'react'; -import { Settings, X, ChevronDown, Key, Globe } from 'lucide-react'; -import { AppConfig, ModelOption, ThinkingLevel } from './types'; -import { getValidThinkingLevels } from './config'; +import { Settings, X } from 'lucide-react'; +import { AppConfig, ModelOption } from './types'; +import ApiSection from './components/settings/ApiSection'; +import ThinkingSection from './components/settings/ThinkingSection'; +import GithubSection from './components/settings/GithubSection'; + +interface SettingsModalProps { + isOpen: boolean; + onClose: () => void; + config: AppConfig; + setConfig: (c: AppConfig) => void; + model: ModelOption; +} const SettingsModal = ({ isOpen, @@ -9,141 +20,42 @@ const SettingsModal = ({ config, setConfig, model -}: { - isOpen: boolean; - onClose: () => void; - config: AppConfig; - setConfig: (c: AppConfig) => void; - model: ModelOption; -}) => { +}: SettingsModalProps) => { if (!isOpen) return null; - const validLevels = getValidThinkingLevels(model); - - const LevelSelect = ({ - label, - value, - onChange, - desc - }: { - label: string, - value: ThinkingLevel, - onChange: (v: ThinkingLevel) => void, - desc: string - }) => ( -
-
- - {value} -
-
- - -
-

{desc}

-
- ); - return (
+ + {/* Header */}

Configuration

-
+ {/* Body */}
- {/* Connection Settings */} -
-
-

API Connection

- {/* Toggle Switch */} - -
- - {config.enableCustomApi && ( -
-
- - setConfig({ ...config, customApiKey: e.target.value })} - className="w-full bg-white border border-slate-200 text-slate-800 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2.5 outline-none placeholder:text-slate-400" - /> -
+ + + -
- - setConfig({ ...config, customBaseUrl: e.target.value })} - className="w-full bg-white border border-slate-200 text-slate-800 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2.5 outline-none placeholder:text-slate-400" - /> -
-
- )} -
- -
-

Thinking Process

- setConfig({ ...config, planningLevel: v })} - desc="Controls the depth of initial query analysis and expert delegation." - /> - - setConfig({ ...config, expertLevel: v })} - desc="Determines how deeply each expert persona thinks about their specific task." - /> - - setConfig({ ...config, synthesisLevel: v })} - desc="Controls the reasoning effort for aggregating results into the final answer." - /> -
+
+ {/* Footer */}
@@ -153,4 +65,4 @@ const SettingsModal = ({ ); }; -export default SettingsModal; \ No newline at end of file +export default SettingsModal; diff --git a/prisma/components/ChatArea.tsx b/prisma/components/ChatArea.tsx index 8dac15e..002e699 100644 --- a/prisma/components/ChatArea.tsx +++ b/prisma/components/ChatArea.tsx @@ -1,7 +1,9 @@ + import React from 'react'; import { ChatMessage, AppState, AnalysisResult, ExpertResult } from '../types'; import ChatMessageItem from './ChatMessage'; import ProcessFlow from './ProcessFlow'; +import Logo from './Logo'; interface ChatAreaProps { messages: ChatMessage[]; @@ -26,12 +28,12 @@ const ChatArea = ({
{messages.length === 0 && appState === 'idle' && ( -
-
- Pr -
-

Prisma

-

Ask a complex question to start.

+
+ +

Prisma

+

+ Deep multi-agent reasoning. +

)} @@ -87,4 +89,4 @@ const ChatArea = ({ ); }; -export default ChatArea; \ No newline at end of file +export default ChatArea; diff --git a/prisma/components/Header.tsx b/prisma/components/Header.tsx index 9977b4c..ba24702 100644 --- a/prisma/components/Header.tsx +++ b/prisma/components/Header.tsx @@ -1,7 +1,9 @@ + import React from 'react'; -import { Settings, ChevronDown, Menu, History } from 'lucide-react'; +import { Settings, ChevronDown, Menu } from 'lucide-react'; import { MODELS } from '../config'; import { ModelOption } from '../types'; +import Logo from './Logo'; interface HeaderProps { selectedModel: ModelOption; @@ -13,7 +15,7 @@ interface HeaderProps { const Header = ({ selectedModel, setSelectedModel, onOpenSettings, onToggleSidebar, onNewChat }: HeaderProps) => { return ( -
+
@@ -65,4 +65,4 @@ const Header = ({ selectedModel, setSelectedModel, onOpenSettings, onToggleSideb ); }; -export default Header; \ No newline at end of file +export default Header; diff --git a/prisma/components/InputSection.tsx b/prisma/components/InputSection.tsx index fa5a32f..6ca82b8 100644 --- a/prisma/components/InputSection.tsx +++ b/prisma/components/InputSection.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useLayoutEffect, useState } from 'react'; +import React, { useRef, useLayoutEffect, useState, useEffect } from 'react'; import { ArrowUp, Square } from 'lucide-react'; import { AppState } from '../types'; @@ -34,6 +34,13 @@ const InputSection = ({ query, setQuery, onRun, onStop, appState }: InputSection } }; + // Focus input on mount and when app becomes idle (e.g. after "New Chat" or completion) + useEffect(() => { + if (appState === 'idle' && textareaRef.current) { + textareaRef.current.focus(); + } + }, [appState]); + // useLayoutEffect prevents visual flickering by adjusting height before paint useLayoutEffect(() => { adjustHeight(); @@ -70,6 +77,7 @@ const InputSection = ({ query, setQuery, onRun, onStop, appState }: InputSection onCompositionEnd={() => setIsComposing(false)} placeholder="Ask a complex question..." rows={1} + autoFocus className="flex-1 max-h-[200px] py-3 pl-4 pr-2 bg-transparent border-none focus:ring-0 resize-none outline-none text-slate-800 placeholder:text-slate-400 leading-relaxed custom-scrollbar text-base" style={{ minHeight: '48px' }} /> diff --git a/prisma/components/Logo.tsx b/prisma/components/Logo.tsx new file mode 100644 index 0000000..ed64ede --- /dev/null +++ b/prisma/components/Logo.tsx @@ -0,0 +1,105 @@ +import React from 'react'; + +interface LogoProps { + className?: string; +} + +const Logo = ({ className = "w-8 h-8" }: LogoProps) => { + return ( + + + {/* Inner Triangle */} + + + {/* Connecting Struts */} + + + + + {/* Outer Triangle */} + + + + + {/* Input Beam */} + + + {/* Blue Beam */} + + + {/* Green Beam */} + + + {/* Purple Beam */} + + + + ); +}; + +export default Logo; \ No newline at end of file diff --git a/prisma/components/MarkdownRenderer.tsx b/prisma/components/MarkdownRenderer.tsx index 737cbee..c440e45 100644 --- a/prisma/components/MarkdownRenderer.tsx +++ b/prisma/components/MarkdownRenderer.tsx @@ -1,5 +1,7 @@ import React, { useState } from 'react'; import ReactMarkdown from 'react-markdown'; +import remarkMath from 'remark-math'; +import rehypeKatex from 'rehype-katex'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism'; import { Copy, Check, Terminal } from 'lucide-react'; @@ -70,14 +72,35 @@ const CodeBlock = ({ node, inline, className, children, ...props }: any) => { }; const MarkdownRenderer = ({ content, className }: { content: string, className?: string }) => { + /** + * Pre-process content to handle common LaTeX delimiters from Gemini + * and optimize Markdown compatibility. + */ + const preprocessMarkdown = (text: string) => { + if (!text) return ""; + + return text + // Replace \[ ... \] with $$ ... $$ + .replace(/\\\[/g, '$$$$') + .replace(/\\\]/g, '$$$$') + // Replace \( ... \) with $ ... $ + .replace(/\\\(/g, '$$') + .replace(/\\\)/g, '$$') + // Fix potential spacing issues between bold marks and math delimiters + .replace(/\*\*(\$)/g, '** $1') + .replace(/(\$)\*\*/g, '$1 **'); + }; + return (
- {content} + {preprocessMarkdown(content)}
); diff --git a/prisma/components/settings/ApiSection.tsx b/prisma/components/settings/ApiSection.tsx new file mode 100644 index 0000000..409d07e --- /dev/null +++ b/prisma/components/settings/ApiSection.tsx @@ -0,0 +1,62 @@ + +import React from 'react'; +import { Key, Globe } from 'lucide-react'; +import { AppConfig } from '../../types'; + +interface ApiSectionProps { + config: AppConfig; + setConfig: (c: AppConfig) => void; +} + +const ApiSection = ({ config, setConfig }: ApiSectionProps) => { + return ( +
+
+

API Connection

+ +
+ + {config.enableCustomApi && ( +
+
+ + setConfig({ ...config, customApiKey: e.target.value })} + className="w-full bg-white border border-slate-200 text-slate-800 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2.5 outline-none placeholder:text-slate-400" + /> +
+ +
+ + setConfig({ ...config, customBaseUrl: e.target.value })} + className="w-full bg-white border border-slate-200 text-slate-800 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2.5 outline-none placeholder:text-slate-400" + /> +
+
+ )} +
+ ); +}; + +export default ApiSection; diff --git a/prisma/components/settings/GithubSection.tsx b/prisma/components/settings/GithubSection.tsx new file mode 100644 index 0000000..49ad7b6 --- /dev/null +++ b/prisma/components/settings/GithubSection.tsx @@ -0,0 +1,50 @@ + +import React, { useState, useEffect } from 'react'; +import { Github, Star } from 'lucide-react'; + +const GithubSection = ({ isOpen }: { isOpen: boolean }) => { + const [stars, setStars] = useState(null); + + useEffect(() => { + if (isOpen) { + fetch('https://api.github.com/repos/yeahhe365/Prisma') + .then(res => res.json()) + .then(data => { + if (data && typeof data.stargazers_count === 'number') { + setStars(data.stargazers_count); + } + }) + .catch(err => console.error("Error fetching stars:", err)); + } + }, [isOpen]); + + return ( + + ); +}; + +export default GithubSection; diff --git a/prisma/components/settings/LevelSelect.tsx b/prisma/components/settings/LevelSelect.tsx new file mode 100644 index 0000000..a72bbc5 --- /dev/null +++ b/prisma/components/settings/LevelSelect.tsx @@ -0,0 +1,42 @@ + +import React from 'react'; +import { ChevronDown } from 'lucide-react'; +import { ThinkingLevel } from '../../types'; + +interface LevelSelectProps { + label: string; + value: ThinkingLevel; + validLevels: ThinkingLevel[]; + onChange: (v: ThinkingLevel) => void; + desc: string; +} + +const LevelSelect = ({ + label, + value, + validLevels, + onChange, + desc +}: LevelSelectProps) => ( +
+
+ + {value} +
+
+ + +
+

{desc}

+
+); + +export default LevelSelect; diff --git a/prisma/components/settings/ThinkingSection.tsx b/prisma/components/settings/ThinkingSection.tsx new file mode 100644 index 0000000..b2513de --- /dev/null +++ b/prisma/components/settings/ThinkingSection.tsx @@ -0,0 +1,69 @@ + +import React from 'react'; +import { RefreshCw } from 'lucide-react'; +import { AppConfig, ModelOption } from '../../types'; +import { getValidThinkingLevels } from '../../config'; +import LevelSelect from './LevelSelect'; + +interface ThinkingSectionProps { + config: AppConfig; + setConfig: (c: AppConfig) => void; + model: ModelOption; +} + +const ThinkingSection = ({ config, setConfig, model }: ThinkingSectionProps) => { + const validLevels = getValidThinkingLevels(model); + + return ( +
+
+

Thinking Process

+
+ +
+
+ +
+

Recursive Refinement

+

Loops expert generation until satisfied.

+
+
+ +
+ + setConfig({ ...config, planningLevel: v })} + desc="Controls the depth of initial query analysis and expert delegation." + /> + + setConfig({ ...config, expertLevel: v })} + desc="Determines how deeply each expert persona thinks about their specific task." + /> + + setConfig({ ...config, synthesisLevel: v })} + desc="Controls the reasoning effort for aggregating results into the final answer." + /> +
+ ); +}; + +export default ThinkingSection; diff --git a/prisma/config.ts b/prisma/config.ts index 5aadfcd..36a8f3d 100644 --- a/prisma/config.ts +++ b/prisma/config.ts @@ -1,4 +1,5 @@ -import { ModelOption, ThinkingLevel } from './types'; + +import { ModelOption, ThinkingLevel, AppConfig } from './types'; export const MODELS: { value: ModelOption; label: string; desc: string }[] = [ { @@ -13,6 +14,22 @@ export const MODELS: { value: ModelOption; label: string; desc: string }[] = [ }, ]; +export const STORAGE_KEYS = { + SETTINGS: 'prisma-settings', + MODEL: 'prisma-selected-model', + SESSION_ID: 'prisma-active-session-id' +}; + +export const DEFAULT_CONFIG: AppConfig = { + planningLevel: 'high', + expertLevel: 'high', + synthesisLevel: 'high', + customApiKey: '', + customBaseUrl: '', + enableCustomApi: false, + enableRecursiveLoop: false +}; + export const getValidThinkingLevels = (model: ModelOption): ThinkingLevel[] => { if (model === 'gemini-3-pro-preview') { return ['low', 'high']; @@ -30,4 +47,4 @@ export const getThinkingBudget = (level: ThinkingLevel, model: ModelOption): num case 'high': return isPro ? 32768 : 16384; default: return 0; } -}; \ No newline at end of file +}; diff --git a/prisma/hooks/useAppLogic.ts b/prisma/hooks/useAppLogic.ts new file mode 100644 index 0000000..d4b0553 --- /dev/null +++ b/prisma/hooks/useAppLogic.ts @@ -0,0 +1,228 @@ + +import { useState, useEffect, useCallback } from 'react'; +import { ModelOption, AppConfig, ChatMessage } from '../types'; +import { STORAGE_KEYS, DEFAULT_CONFIG, getValidThinkingLevels } from '../config'; +import { useDeepThink } from './useDeepThink'; +import { useChatSessions } from './useChatSessions'; +import { setInterceptorUrl } from '../interceptor'; + +export const useAppLogic = () => { + // Session Management + const { + sessions, + currentSessionId, + setCurrentSessionId, + createSession, + updateSessionMessages, + deleteSession, + getSession + } = useChatSessions(); + + // UI State + const [isSidebarOpen, setIsSidebarOpen] = useState(true); + const [isSettingsOpen, setIsSettingsOpen] = useState(false); + + // Active Chat State + const [messages, setMessages] = useState([]); + const [query, setQuery] = useState(''); + + // App Configuration with Persistence + const [selectedModel, setSelectedModel] = useState(() => { + const cached = localStorage.getItem(STORAGE_KEYS.MODEL); + return (cached as ModelOption) || 'gemini-3-flash-preview'; + }); + + const [config, setConfig] = useState(() => { + const cached = localStorage.getItem(STORAGE_KEYS.SETTINGS); + if (cached) { + try { + return { ...DEFAULT_CONFIG, ...JSON.parse(cached) }; + } catch (e) { + return DEFAULT_CONFIG; + } + } + return DEFAULT_CONFIG; + }); + + // Deep Think Engine + const { + appState, + managerAnalysis, + experts, + finalOutput, + synthesisThoughts, + runDynamicDeepThink, + stopDeepThink, + resetDeepThink, + processStartTime, + processEndTime + } = useDeepThink(); + + // Network Interceptor Sync + useEffect(() => { + if (config.enableCustomApi && config.customBaseUrl) { + setInterceptorUrl(config.customBaseUrl); + } else { + setInterceptorUrl(null); + } + return () => setInterceptorUrl(null); + }, [config.enableCustomApi, config.customBaseUrl]); + + // Persistence Effects + useEffect(() => { + localStorage.setItem(STORAGE_KEYS.SETTINGS, JSON.stringify(config)); + }, [config]); + + useEffect(() => { + localStorage.setItem(STORAGE_KEYS.MODEL, selectedModel); + }, [selectedModel]); + + useEffect(() => { + const cachedSessionId = localStorage.getItem(STORAGE_KEYS.SESSION_ID); + if (cachedSessionId && sessions.some(s => s.id === cachedSessionId)) { + setCurrentSessionId(cachedSessionId); + } + }, [sessions, setCurrentSessionId]); + + useEffect(() => { + if (currentSessionId) { + localStorage.setItem(STORAGE_KEYS.SESSION_ID, currentSessionId); + } else { + localStorage.removeItem(STORAGE_KEYS.SESSION_ID); + } + }, [currentSessionId]); + + // Handle Model Constraints + useEffect(() => { + const validLevels = getValidThinkingLevels(selectedModel); + setConfig(prev => { + const newPlanning = validLevels.includes(prev.planningLevel) ? prev.planningLevel : 'low'; + const newExpert = validLevels.includes(prev.expertLevel) ? prev.expertLevel : 'low'; + const newSynthesis = validLevels.includes(prev.synthesisLevel) ? prev.synthesisLevel : 'high'; + + if (newPlanning !== prev.planningLevel || newExpert !== prev.expertLevel || newSynthesis !== prev.synthesisLevel) { + return { + ...prev, + planningLevel: newPlanning as any, + expertLevel: newExpert as any, + synthesisLevel: newSynthesis as any, + }; + } + return prev; + }); + }, [selectedModel]); + + // Sync Messages when switching sessions + useEffect(() => { + if (currentSessionId) { + const session = getSession(currentSessionId); + if (session) { + setMessages(session.messages); + setSelectedModel(session.model || 'gemini-3-flash-preview'); + } + } else { + setMessages([]); + } + }, [currentSessionId, getSession]); + + // Handle AI Completion + useEffect(() => { + if (appState === 'completed') { + const finalizedMessage: ChatMessage = { + id: `ai-${Date.now()}`, + role: 'model', + content: finalOutput, + analysis: managerAnalysis, + experts: experts, + synthesisThoughts: synthesisThoughts, + isThinking: false, + totalDuration: (processStartTime && processEndTime) ? (processEndTime - processStartTime) : undefined + }; + + const newMessages = [...messages, finalizedMessage]; + setMessages(newMessages); + + if (currentSessionId) { + updateSessionMessages(currentSessionId, newMessages); + } else { + createSession(newMessages, selectedModel); + } + + resetDeepThink(); + } + }, [appState, finalOutput, managerAnalysis, experts, synthesisThoughts, resetDeepThink, processStartTime, processEndTime, currentSessionId, messages, selectedModel, createSession, updateSessionMessages]); + + const handleRun = useCallback(() => { + if (!query.trim()) return; + + const userMsg: ChatMessage = { + id: `user-${Date.now()}`, + role: 'user', + content: query + }; + + const newMessages = [...messages, userMsg]; + setMessages(newMessages); + + let activeSessionId = currentSessionId; + if (!activeSessionId) { + activeSessionId = createSession(newMessages, selectedModel); + } else { + updateSessionMessages(activeSessionId, newMessages); + } + + runDynamicDeepThink(query, messages, selectedModel, config); + setQuery(''); + }, [query, messages, currentSessionId, selectedModel, config, createSession, updateSessionMessages, runDynamicDeepThink]); + + const handleNewChat = useCallback(() => { + stopDeepThink(); + setCurrentSessionId(null); + setMessages([]); + setQuery(''); + resetDeepThink(); + if (window.innerWidth < 1024) setIsSidebarOpen(false); + }, [stopDeepThink, setCurrentSessionId, resetDeepThink]); + + const handleSelectSession = useCallback((id: string) => { + stopDeepThink(); + resetDeepThink(); + setCurrentSessionId(id); + if (window.innerWidth < 1024) setIsSidebarOpen(false); + }, [stopDeepThink, resetDeepThink, setCurrentSessionId]); + + const handleDeleteSession = useCallback((id: string, e: React.MouseEvent) => { + e.stopPropagation(); + deleteSession(id); + if (currentSessionId === id) { + handleNewChat(); + } + }, [deleteSession, currentSessionId, handleNewChat]); + + return { + sessions, + currentSessionId, + messages, + query, + setQuery, + selectedModel, + setSelectedModel, + config, + setConfig, + isSidebarOpen, + setIsSidebarOpen, + isSettingsOpen, + setIsSettingsOpen, + appState, + managerAnalysis, + experts, + finalOutput, + processStartTime, + processEndTime, + handleRun, + handleNewChat, + handleSelectSession, + handleDeleteSession, + stopDeepThink + }; +}; diff --git a/prisma/hooks/useDeepThink.ts b/prisma/hooks/useDeepThink.ts index be65da8..35388a2 100644 --- a/prisma/hooks/useDeepThink.ts +++ b/prisma/hooks/useDeepThink.ts @@ -1,52 +1,37 @@ -import { useState, useRef, useCallback } from 'react'; + +import { useCallback } from 'react'; import { getAI } from '../api'; import { getThinkingBudget } from '../config'; -import { AppConfig, ModelOption, AppState, AnalysisResult, ExpertResult, ChatMessage } from '../types'; +import { AppConfig, ModelOption, ExpertResult, ChatMessage } from '../types'; -import { executeManagerAnalysis } from '../services/deepThink/manager'; +import { executeManagerAnalysis, executeManagerReview } from '../services/deepThink/manager'; import { streamExpertResponse } from '../services/deepThink/expert'; import { streamSynthesisResponse } from '../services/deepThink/synthesis'; +import { useDeepThinkState } from './useDeepThinkState'; export const useDeepThink = () => { - const [appState, setAppState] = useState('idle'); - const [managerAnalysis, setManagerAnalysis] = useState(null); - const [experts, setExperts] = useState([]); - const [finalOutput, setFinalOutput] = useState(''); - const [synthesisThoughts, setSynthesisThoughts] = useState(''); - - // Timing state - const [processStartTime, setProcessStartTime] = useState(null); - const [processEndTime, setProcessEndTime] = useState(null); + const { + appState, setAppState, + managerAnalysis, setManagerAnalysis, + experts, expertsDataRef, + finalOutput, setFinalOutput, + synthesisThoughts, setSynthesisThoughts, + processStartTime, setProcessStartTime, + processEndTime, setProcessEndTime, + abortControllerRef, + resetDeepThink, + stopDeepThink, + updateExpertAt, + setInitialExperts, + appendExperts + } = useDeepThinkState(); - // Refs for data consistency during high-frequency streaming updates - const expertsDataRef = useRef([]); - const abortControllerRef = useRef(null); - - const stopDeepThink = useCallback(() => { - if (abortControllerRef.current) { - abortControllerRef.current.abort(); - abortControllerRef.current = null; - } - setAppState('idle'); - setProcessEndTime(Date.now()); - }, []); - - const resetDeepThink = useCallback(() => { - setAppState('idle'); - setManagerAnalysis(null); - setExperts([]); - expertsDataRef.current = []; - setFinalOutput(''); - setSynthesisThoughts(''); - setProcessStartTime(null); - setProcessEndTime(null); - abortControllerRef.current = null; - }, []); - - // Helper: Orchestrate a single expert's lifecycle (Start -> Stream -> End) + /** + * Orchestrates a single expert's lifecycle (Start -> Stream -> End) + */ const runExpertLifecycle = async ( expert: ExpertResult, - index: number, + globalIndex: number, ai: any, model: ModelOption, context: string, @@ -55,17 +40,10 @@ export const useDeepThink = () => { ): Promise => { if (signal.aborted) return expert; - // 1. Mark as thinking const startTime = Date.now(); - expertsDataRef.current[index] = { - ...expert, - status: 'thinking', - startTime - }; - setExperts([...expertsDataRef.current]); + updateExpertAt(globalIndex, { status: 'thinking', startTime }); try { - // 2. Stream execution via service let fullContent = ""; let fullThoughts = ""; @@ -79,44 +57,27 @@ export const useDeepThink = () => { (textChunk, thoughtChunk) => { fullContent += textChunk; fullThoughts += thoughtChunk; - - // Update Ref & State live - expertsDataRef.current[index] = { - ...expertsDataRef.current[index], - thoughts: fullThoughts, - content: fullContent - }; - setExperts([...expertsDataRef.current]); + updateExpertAt(globalIndex, { thoughts: fullThoughts, content: fullContent }); } ); - if (signal.aborted) return expertsDataRef.current[index]; + if (signal.aborted) return expertsDataRef.current[globalIndex]; - // 3. Mark as completed - expertsDataRef.current[index] = { - ...expertsDataRef.current[index], - status: 'completed', - endTime: Date.now() - }; - setExperts([...expertsDataRef.current]); - - return expertsDataRef.current[index]; + updateExpertAt(globalIndex, { status: 'completed', endTime: Date.now() }); + return expertsDataRef.current[globalIndex]; } catch (error) { console.error(`Expert ${expert.role} error:`, error); if (!signal.aborted) { - expertsDataRef.current[index] = { - ...expertsDataRef.current[index], - status: 'error', - content: "Failed to generate response.", - endTime: Date.now() - }; - setExperts([...expertsDataRef.current]); + updateExpertAt(globalIndex, { status: 'error', content: "Failed to generate response.", endTime: Date.now() }); } - return expertsDataRef.current[index]; + return expertsDataRef.current[globalIndex]; } }; + /** + * Main Orchestration logic + */ const runDynamicDeepThink = async ( query: string, history: ChatMessage[], @@ -125,20 +86,16 @@ export const useDeepThink = () => { ) => { if (!query.trim()) return; - // Reset previous run - if (abortControllerRef.current) { - abortControllerRef.current.abort(); - } + if (abortControllerRef.current) abortControllerRef.current.abort(); abortControllerRef.current = new AbortController(); const signal = abortControllerRef.current.signal; + // Reset UI state setAppState('analyzing'); setManagerAnalysis(null); - setExperts([]); - expertsDataRef.current = []; + setInitialExperts([]); setFinalOutput(''); setSynthesisThoughts(''); - setProcessStartTime(Date.now()); setProcessEndTime(null); @@ -152,33 +109,8 @@ export const useDeepThink = () => { `${msg.role === 'user' ? 'User' : 'Model'}: ${msg.content}` ).join('\n'); - // --- 1. Initialize Primary Expert IMMEDIATELY --- - const primaryExpert: ExpertResult = { - id: 'expert-0', - role: "Primary Responder", - description: "Directly addresses the user's original query.", - temperature: 1, - prompt: query, - status: 'pending' - }; - - expertsDataRef.current = [primaryExpert]; - setExperts([primaryExpert]); - - // --- 2. Start Parallel Execution --- + // --- Phase 1: Planning & Initial Experts --- - // Task A: Run Primary Expert (Index 0) - const primaryExpertTask = runExpertLifecycle( - primaryExpert, - 0, - ai, - model, - recentHistory, - getThinkingBudget(config.expertLevel, model), - signal - ); - - // Task B: Run Manager Analysis via Service const managerTask = executeManagerAnalysis( ai, model, @@ -187,60 +119,97 @@ export const useDeepThink = () => { getThinkingBudget(config.planningLevel, model) ); - // Wait for Manager Analysis - const analysisJson = await managerTask; + const primaryExpert: ExpertResult = { + id: 'expert-0', + role: "Primary Responder", + description: "Directly addresses the user's original query.", + temperature: 1, + prompt: query, + status: 'pending', + round: 1 + }; + setInitialExperts([primaryExpert]); + + const primaryTask = runExpertLifecycle( + primaryExpert, 0, ai, model, recentHistory, + getThinkingBudget(config.expertLevel, model), signal + ); + + const analysisJson = await managerTask; if (signal.aborted) return; setManagerAnalysis(analysisJson); - // --- 3. Initialize & Run Supplementary Experts --- - - const generatedExperts: ExpertResult[] = analysisJson.experts.map((exp, idx) => ({ + const round1Experts: ExpertResult[] = analysisJson.experts.map((exp, idx) => ({ ...exp, - id: `expert-${idx + 1}`, - status: 'pending' + id: `expert-r1-${idx + 1}`, + status: 'pending', + round: 1 })); - // Update state: Keep Primary (0) and append new ones - const currentPrimary = expertsDataRef.current[0]; - const allExperts = [currentPrimary, ...generatedExperts]; - expertsDataRef.current = allExperts; - setExperts([...allExperts]); - + appendExperts(round1Experts); setAppState('experts_working'); - // Task C: Run Supplementary Experts (Offset indices by 1) - const supplementaryTasks = generatedExperts.map((exp, idx) => - runExpertLifecycle( - exp, - idx + 1, - ai, - model, - recentHistory, - getThinkingBudget(config.expertLevel, model), - signal - ) + const round1Tasks = round1Experts.map((exp, idx) => + runExpertLifecycle(exp, idx + 1, ai, model, recentHistory, + getThinkingBudget(config.expertLevel, model), signal) ); - // --- 4. Wait for ALL Experts --- - const allResults = await Promise.all([primaryExpertTask, ...supplementaryTasks]); + await Promise.all([primaryTask, ...round1Tasks]); + if (signal.aborted) return; + + // --- Phase 2: Recursive Loop (Optional) --- + let roundCounter = 1; + const MAX_ROUNDS = 3; + let loopActive = config.enableRecursiveLoop ?? false; + + while (loopActive && roundCounter < MAX_ROUNDS) { + if (signal.aborted) return; + setAppState('reviewing'); + + const reviewResult = await executeManagerReview( + ai, model, query, expertsDataRef.current, + getThinkingBudget(config.planningLevel, model) + ); + + if (signal.aborted) return; + if (reviewResult.satisfied) { + loopActive = false; + } else { + roundCounter++; + const nextRoundExperts = (reviewResult.refined_experts || []).map((exp, idx) => ({ + ...exp, id: `expert-r${roundCounter}-${idx}`, status: 'pending' as const, round: roundCounter + })); + + if (nextRoundExperts.length === 0) { + loopActive = false; + break; + } + + const startIndex = expertsDataRef.current.length; + appendExperts(nextRoundExperts); + setAppState('experts_working'); + + const nextRoundTasks = nextRoundExperts.map((exp, idx) => + runExpertLifecycle(exp, startIndex + idx, ai, model, recentHistory, + getThinkingBudget(config.expertLevel, model), signal) + ); + + await Promise.all(nextRoundTasks); + } + } if (signal.aborted) return; - // --- 5. Synthesis --- + // --- Phase 3: Synthesis --- setAppState('synthesizing'); let fullFinalText = ''; let fullFinalThoughts = ''; await streamSynthesisResponse( - ai, - model, - query, - recentHistory, - allResults, - getThinkingBudget(config.synthesisLevel, model), - signal, + ai, model, query, recentHistory, expertsDataRef.current, + getThinkingBudget(config.synthesisLevel, model), signal, (textChunk, thoughtChunk) => { fullFinalText += textChunk; fullFinalThoughts += thoughtChunk; @@ -255,9 +224,7 @@ export const useDeepThink = () => { } } catch (e: any) { - if (signal.aborted) { - console.log('Operation aborted by user'); - } else { + if (!signal.aborted) { console.error(e); setAppState('idle'); setProcessEndTime(Date.now()); diff --git a/prisma/hooks/useDeepThinkState.ts b/prisma/hooks/useDeepThinkState.ts new file mode 100644 index 0000000..5015dc5 --- /dev/null +++ b/prisma/hooks/useDeepThinkState.ts @@ -0,0 +1,73 @@ + +import { useState, useRef, useCallback } from 'react'; +import { AppState, AnalysisResult, ExpertResult } from '../types'; + +export const useDeepThinkState = () => { + const [appState, setAppState] = useState('idle'); + const [managerAnalysis, setManagerAnalysis] = useState(null); + const [experts, setExperts] = useState([]); + const [finalOutput, setFinalOutput] = useState(''); + const [synthesisThoughts, setSynthesisThoughts] = useState(''); + + // Timing state + const [processStartTime, setProcessStartTime] = useState(null); + const [processEndTime, setProcessEndTime] = useState(null); + + // Refs for data consistency + const expertsDataRef = useRef([]); + const abortControllerRef = useRef(null); + + const resetDeepThink = useCallback(() => { + setAppState('idle'); + setManagerAnalysis(null); + setExperts([]); + expertsDataRef.current = []; + setFinalOutput(''); + setSynthesisThoughts(''); + setProcessStartTime(null); + setProcessEndTime(null); + abortControllerRef.current = null; + }, []); + + const stopDeepThink = useCallback(() => { + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + abortControllerRef.current = null; + } + setAppState('idle'); + setProcessEndTime(Date.now()); + }, []); + + const updateExpertAt = useCallback((index: number, update: Partial | ((prev: ExpertResult) => ExpertResult)) => { + const current = expertsDataRef.current[index]; + const next = typeof update === 'function' ? update(current) : { ...current, ...update }; + expertsDataRef.current[index] = next; + setExperts([...expertsDataRef.current]); + }, []); + + const setInitialExperts = useCallback((initialList: ExpertResult[]) => { + expertsDataRef.current = initialList; + setExperts(initialList); + }, []); + + const appendExperts = useCallback((newList: ExpertResult[]) => { + expertsDataRef.current = [...expertsDataRef.current, ...newList]; + setExperts([...expertsDataRef.current]); + }, []); + + return { + appState, setAppState, + managerAnalysis, setManagerAnalysis, + experts, setExperts, expertsDataRef, + finalOutput, setFinalOutput, + synthesisThoughts, setSynthesisThoughts, + processStartTime, setProcessStartTime, + processEndTime, setProcessEndTime, + abortControllerRef, + resetDeepThink, + stopDeepThink, + updateExpertAt, + setInitialExperts, + appendExperts + }; +}; diff --git a/prisma/index.html b/prisma/index.html index 21a72ba..efe80fc 100644 --- a/prisma/index.html +++ b/prisma/index.html @@ -7,6 +7,7 @@ +