11import Markdown from "@/components/ui/markdown" ;
2+ import { Textarea } from "@/components/ui/textarea" ;
3+ import { AITools } from "@/core/service/dataManageService/aiEngine/AITools" ;
24import { Settings } from "@/core/service/Settings" ;
35import { SubWindow } from "@/core/service/SubWindow" ;
46import { activeProjectAtom } from "@/state" ;
57import SettingsWindow from "@/sub/SettingsWindow" ;
68import { Vector } from "@graphif/data-structures" ;
79import { Rectangle } from "@graphif/shapes" ;
810import { useAtom } from "jotai" ;
9- import { Bot , FolderOpen , Loader2 , Send , SettingsIcon , User } from "lucide-react" ;
11+ import { Bot , FolderOpen , Loader2 , Send , SettingsIcon , User , Wrench } from "lucide-react" ;
1012import OpenAI from "openai" ;
1113import { useRef , useState } from "react" ;
1214
1315export default function AIWindow ( ) {
1416 const [ project ] = useAtom ( activeProjectAtom ) ;
1517 const [ inputValue , setInputValue ] = useState ( "" ) ;
1618 const [ messages , setMessages ] = useState < ( OpenAI . ChatCompletionMessageParam & { tokens ?: number } ) [ ] > ( [
17- // {
18- // role: "system",
19- // content: `\
20- // **角色:** Project Graph AI (首席工程师,严格遵循 ReAct 模式)
21- // **核心原则:**
22- // 1. **深度优先探索:** 系统性探索用户视野内节点及其父子节点,理解内容、结构、关系。
23- // 2. **基于事实行动:** 收集充分上下文前不修改节点。分析工具输出,留意新节点/关系线索。
24- // 3. **最小有效修改:** 仅进行完成任务必需的最少更改,保持图结构整洁。
25- // 4. **高度自主:** 优先使用工具解决问题。仅在多次尝试失败且信息关键时请求用户输入。
26- // 5. **专注任务:** 避免无关对话,专注迭代(思考->行动->观察)直至任务完成。
27- // **关键指令:**
28- // * **ReAct 循环:** 推理->调用工具->观察结果->迭代。
29- // * **工具输出关键:** 仔细分析所有输出,主动探索输出中提到的新相关节点。
30- // * **根因分析:** 识别问题节点及其潜在关联节点(可能为根本原因)。
31- // `,
32- // },
19+ {
20+ role : "system" ,
21+ content : "尽可能尝试使用工具解决问题,如果实在不行才能问用户" ,
22+ } ,
3323 ] ) ;
3424 const [ requesting , setRequesting ] = useState ( false ) ;
3525 const [ totalInputTokens , setTotalInputTokens ] = useState ( 0 ) ;
@@ -40,11 +30,11 @@ export default function AIWindow() {
4030 function addMessage ( message : OpenAI . ChatCompletionMessageParam & { tokens ?: number } ) {
4131 setMessages ( ( prev ) => [ ...prev , message ] ) ;
4232 }
43- function setLastMessageContent ( content : string ) {
33+ function setLastMessage ( msg : OpenAI . ChatCompletionMessageParam ) {
4434 setMessages ( ( prev ) => {
45- const lastMessage = prev [ prev . length - 1 ] ;
46- if ( ! lastMessage ) return prev ;
47- return [ ... prev . slice ( 0 , - 1 ) , { ... lastMessage , content } as any ] ;
35+ const newMessages = [ ... prev ] ;
36+ newMessages [ newMessages . length - 1 ] = msg ;
37+ return newMessages ;
4838 } ) ;
4939 }
5040
@@ -54,42 +44,89 @@ export default function AIWindow() {
5444 }
5545 }
5646
57- async function send ( ) {
47+ async function run ( msgs : OpenAI . ChatCompletionMessageParam [ ] = [ ... messages , { role : "user" , content : inputValue } ] ) {
5848 if ( ! project ) return ;
5949 scrollToBottom ( ) ;
6050 setRequesting ( true ) ;
61- setInputValue ( "" ) ;
62- const msgs : OpenAI . ChatCompletionMessageParam [ ] = [
63- ...messages ,
64- {
65- role : "user" ,
66- content : inputValue ,
67- } ,
68- ] ;
69- addMessage ( {
70- role : "user" ,
71- content : inputValue ,
72- } ) ;
73- addMessage ( {
74- role : "assistant" ,
75- content : "Requesting..." ,
76- } ) ;
77- const stream = await project . aiEngine . chat ( msgs ) ;
78- let streamingMsg = "" ;
79- let lastChunk : OpenAI . ChatCompletionChunk | null = null ;
80- for await ( const chunk of stream ) {
81- const delta = chunk . choices [ 0 ] . delta ;
82- streamingMsg += delta . content ;
83- setLastMessageContent ( streamingMsg ) ;
51+ try {
52+ const stream = await project . aiEngine . chat ( msgs ) ;
53+ addMessage ( {
54+ role : "assistant" ,
55+ content : "Requesting..." ,
56+ } ) ;
57+ const streamingMsg : OpenAI . ChatCompletionAssistantMessageParam = {
58+ role : "assistant" ,
59+ content : "" ,
60+ tool_calls : [ ] ,
61+ } ;
62+ let lastChunk : OpenAI . ChatCompletionChunk | null = null ;
63+ for await ( const chunk of stream ) {
64+ const delta = chunk . choices [ 0 ] . delta ;
65+ streamingMsg . content ! += delta . content ?? "" ;
66+ const toolCalls = delta . tool_calls || [ ] ;
67+ for ( const toolCall of toolCalls ) {
68+ if ( typeof streamingMsg . tool_calls !== "undefined" ) {
69+ const index = streamingMsg . tool_calls . length ;
70+ if ( ! streamingMsg . tool_calls [ index ] ) {
71+ streamingMsg . tool_calls [ index ] = {
72+ ...toolCall ,
73+ // Google AI 不会返回工具调用的 id
74+ // https://discuss.ai.google.dev/t/tool-calling-with-openai-api-not-working/60140/5
75+ id : toolCall . id || crypto . randomUUID ( ) ,
76+ } as any ;
77+ } else if ( toolCall . function ) {
78+ streamingMsg . tool_calls [ index ] . function . arguments += toolCall . function . arguments ;
79+ }
80+ }
81+ }
82+ setLastMessage ( streamingMsg ) ;
83+ scrollToBottom ( ) ;
84+ lastChunk = chunk ;
85+ }
86+ setRequesting ( false ) ;
87+ if ( ! lastChunk ) return ;
88+ if ( ! lastChunk . usage ) return ;
89+ setTotalInputTokens ( ( v ) => v + lastChunk . usage ! . prompt_tokens ) ;
90+ setTotalOutputTokens ( ( v ) => v + lastChunk . usage ! . completion_tokens ) ;
8491 scrollToBottom ( ) ;
85- lastChunk = chunk ;
92+ // 如果有工具调用,执行工具调用
93+ if ( streamingMsg . tool_calls && streamingMsg . tool_calls . length > 0 ) {
94+ const toolMsgs : OpenAI . ChatCompletionToolMessageParam [ ] = [ ] ;
95+ for ( const toolCall of streamingMsg . tool_calls ) {
96+ const tool = AITools . handlers . get ( toolCall . function . name ) ;
97+ if ( ! tool ) {
98+ return ;
99+ }
100+ let observation = "" ;
101+ try {
102+ const result = await tool ( project , JSON . parse ( toolCall . function . arguments ) ) ;
103+ if ( typeof result === "string" ) {
104+ observation = result ;
105+ } else if ( typeof result === "object" ) {
106+ observation = JSON . stringify ( result ) ;
107+ } else {
108+ observation = String ( result ) ;
109+ }
110+ } catch ( e ) {
111+ observation = `工具调用失败:${ ( e as Error ) . message } ` ;
112+ }
113+ const msg = {
114+ role : "tool" as const ,
115+ content : observation ,
116+ tool_call_id : toolCall . id ! ,
117+ } ;
118+ addMessage ( msg ) ;
119+ toolMsgs . push ( msg ) ;
120+ }
121+ // 工具调用结束后,重新发送消息,让模型继续思考
122+ run ( [ ...msgs , streamingMsg , ...toolMsgs ] ) ;
123+ }
124+ } catch ( e ) {
125+ addMessage ( {
126+ role : "assistant" ,
127+ content : String ( e ) ,
128+ } ) ;
86129 }
87- setRequesting ( false ) ;
88- if ( ! lastChunk ) return ;
89- if ( ! lastChunk . usage ) return ;
90- setTotalInputTokens ( ( v ) => v + lastChunk . usage ! . prompt_tokens ) ;
91- setTotalOutputTokens ( ( v ) => v + lastChunk . usage ! . completion_tokens ) ;
92- scrollToBottom ( ) ;
93130 }
94131
95132 return project ? (
@@ -98,43 +135,54 @@ export default function AIWindow() {
98135 { messages . map ( ( msg , i ) =>
99136 msg . role === "user" ? (
100137 < div key = { i } className = "flex justify-end" >
101- < div className = "max-w-11/12 rounded-2xl rounded-br-none px-3 py-2" > { msg . content as string } </ div >
138+ < div className = "max-w-11/12 bg-accent text-accent-foreground rounded-2xl rounded-br-none px-3 py-2" >
139+ { msg . content as string }
140+ </ div >
102141 </ div >
103142 ) : msg . role === "assistant" ? (
104- < div key = { i } >
105- < Markdown source = { msg . content as string } />
143+ < div key = { i } className = "flex flex-col gap-2" >
144+ { msg . content && < Markdown source = { msg . content as string } /> }
145+ { msg . tool_calls &&
146+ msg . tool_calls . map ( ( toolCall ) => (
147+ < div className = "flex items-center gap-1 text-xs" key = { toolCall . id } >
148+ < Wrench size = { 16 } />
149+ { toolCall . function . name }
150+ { /*{toolCall.function.arguments}*/ }
151+ </ div >
152+ ) ) }
106153 </ div >
107154 ) : (
108155 < > </ >
109156 ) ,
110157 ) }
111158 </ div >
112- < div className = "flex flex-col gap-2 rounded-xl border p -2" >
113- < div className = "flex gap-2" >
114- < SettingsIcon className = "cursor-pointer" onClick = { ( ) => SettingsWindow . open ( "settings" ) } />
115- { showTokenCount && (
116- < >
117- < div className = "flex-1" > </ div >
118- < User / >
119- < span > { totalInputTokens } </ span >
120- < Bot / >
121- < span > { totalOutputTokens } </ span >
122- </ >
123- ) }
124- < div className = "flex-1" > </ div >
125- { requesting ? (
126- < Loader2 className = "animate-spin" />
127- ) : (
128- < Send className = "cursor-pointer" onClick = { ( ) => send ( ) } />
129- ) }
130- </ div >
131- < textarea
132- className = "cursor-text outline-none"
133- placeholder = "What can I say?"
134- onChange = { ( e ) => setInputValue ( e . target . value ) }
135- value = { inputValue }
136- />
159+ < div className = "mb-2 flex gap -2" >
160+ < SettingsIcon className = "cursor-pointer" onClick = { ( ) => SettingsWindow . open ( "settings" ) } / >
161+ { showTokenCount && (
162+ < >
163+ < div className = "flex-1" > </ div >
164+ < User / >
165+ < span > { totalInputTokens } </ span >
166+ < Bot / >
167+ < span > { totalOutputTokens } </ span >
168+ </ >
169+ ) }
170+ < div className = "flex-1" > </ div >
171+ { requesting ? (
172+ < Loader2 className = "animate-spin" />
173+ ) : (
174+ < Send
175+ className = "cursor-pointer"
176+ onClick = { ( ) => {
177+ if ( ! inputValue . trim ( ) ) return ;
178+ addMessage ( { role : "user" , content : inputValue } ) ;
179+ setInputValue ( "" ) ;
180+ run ( ) ;
181+ } }
182+ />
183+ ) }
137184 </ div >
185+ < Textarea placeholder = "What can I say?" onChange = { ( e ) => setInputValue ( e . target . value ) } value = { inputValue } />
138186 </ div >
139187 ) : (
140188 < div className = "flex flex-col gap-2 p-8" >
0 commit comments