File size: 6,154 Bytes
c2c8c8d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
import { useCallback, useRef } from 'react';
import { useAIStore } from '@/stores/aiStore';
import { useEditorStore } from '@/stores/editorStore';
import { useFileStore } from '@/stores/fileStore';
import { useEnvStore } from '@/stores/envStore';
import { parseMarkdown, MarkdownSegment } from '@/utils/markdownParser';

const API_URL = import.meta.env.VITE_API_URL || '/api/v1';

const getSystemPrompt = (envType: string | null) => {
  const lang = envType === 'python' ? 'Python 3' : envType === 'java' ? 'Java (JDK 17)' : 'HTML, CSS, JavaScript, TypeScript, React, and web technologies';
  return `You are GLMPilot AI, an expert development assistant. You help with ${lang}.
CRITICAL INSTRUCTION: When you provide code that should be applied to a file, you MUST start the code block with a markdown bolded filename, followed immediately by the fenced code block. Example:
**\`src/App.tsx\`**
\`\`\`tsx
export default function App() { ... }
\`\`\`
Do not omit the bolded filename. Provide exact, complete code drop-ins when requested to fix or change code.`;
};

export function useGLMChat() {
  const { addMessage, updateStreamingMessage, finishStreaming, setIsStreaming, isStreaming } =
    useAIStore();
  const abortRef = useRef<AbortController | null>(null);

  const sendMessage = useCallback(
    async (message: string, history: Array<{ role: string; content: string }> = []) => {
      addMessage({
        id: `msg-${Date.now()}`,
        role: 'user',
        content: message,
        timestamp: Date.now(),
      });

      setIsStreaming(true);
      const controller = new AbortController();
      abortRef.current = controller;

      try {
        // Build the dynamic workspace context from the stores
        const editorStore = useEditorStore.getState();
        const fileStore = useFileStore.getState();
        const envStore = useEnvStore.getState();
        
        // Merge the base files with the unsaved editor changes
        const currentFiles = { ...fileStore.files };
        for (const [path, file] of Object.entries(editorStore.openFiles)) {
          currentFiles[path] = typeof file === 'string' ? file : file.content;
        }

        let dynamicSystemPrompt = getSystemPrompt(envStore.environment) + '\n\n--- CURRENT WORKSPACE FILES ---\n';
        for (const [path, content] of Object.entries(currentFiles)) {
          // Guess language for markdown
          const ext = path.split('.').pop() || 'text';
          const langMap: Record<string, string> = { js: 'javascript', ts: 'typescript', jsx: 'jsx', tsx: 'tsx', html: 'html', css: 'css' };
          const lang = langMap[ext] || ext;
          
          dynamicSystemPrompt += `**\`${path}\`**\n\`\`\`${lang}\n${content}\n\`\`\`\n\n`;
        }

        const response = await fetch(`${API_URL}/chat`, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({
            messages: [
              { role: 'system', content: dynamicSystemPrompt },
              ...history,
              { role: 'user', content: message },
            ],
            stream: true,
            temperature: 0.7,
            max_tokens: 4096,
          }),
          signal: controller.signal,
        });

        if (!response.ok) {
          const errText = await response.text();
          throw new Error(`GLM API error ${response.status}: ${errText}`);
        }

        const reader = response.body?.getReader();
        if (!reader) throw new Error('No response body');

        const decoder = new TextDecoder();
        let fullContent = '';
        let buffer = '';

        while (true) {
          const { done, value } = await reader.read();
          if (done) break;

          buffer += decoder.decode(value, { stream: true });
          const lines = buffer.split('\n');
          buffer = lines.pop() || '';

          for (const line of lines) {
            const trimmed = line.trim();
            if (!trimmed.startsWith('data: ')) continue;
            const data = trimmed.slice(6);
            if (data === '[DONE]') break;

            try {
              const parsed = JSON.parse(data);
              const token = parsed.choices?.[0]?.delta?.content;
              if (token) {
                fullContent += token;
                updateStreamingMessage(token);
              }
            } catch {
              /* skip malformed chunks */
            }
          }
        }

        // Lovable-style Auto-Apply logic:
        // Parse the full completion string for markdown code blocks + filenames
        const segments = parseMarkdown(fullContent);
        
        for (const segment of segments) {
          if (segment.type === 'code' && segment.filename && segment.code) {
            const filename = segment.filename;
            const code = segment.code;
            
            // Apply it! 
            if (editorStore.openFiles[filename]) {
              editorStore.updateContent(filename, code);
            } else if (fileStore.files[filename] !== undefined) {
              fileStore.updateFile(filename, code);
            } else {
              fileStore.createFile(filename, code);
            }
          }
        }

        finishStreaming(fullContent);
      } catch (error) {
        if ((error as Error).name !== 'AbortError') {
          const msg = (error as Error).message || 'Unknown error';
          finishStreaming(
            `Sorry, something went wrong.\n\n${msg.length > 400 ? `${msg.slice(0, 400)}…` : msg}`
          );
          console.error('[useGLMChat]', error);
        } else {
          // User stopped — commit whatever was streamed so far
          const partial = useAIStore.getState().streamingMessage;
          if (partial) finishStreaming(partial);
        }
      } finally {
        abortRef.current = null;
      }
    },
    [addMessage, updateStreamingMessage, finishStreaming, setIsStreaming]
  );

  const stopGeneration = useCallback(() => {
    abortRef.current?.abort();
    setIsStreaming(false);
  }, [setIsStreaming]);

  return { sendMessage, stopGeneration, isStreaming };
}