vzcode 2.22.0 → 2.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{buildWorker-Bi6wsfQk.js → buildWorker-DylWm2VX.js} +64 -72
- package/dist/assets/index-CkFweu5-.js +403 -0
- package/dist/assets/{index-fSroLVgi.css → index-Dyt9tpmu.css} +1 -5
- package/dist/assets/{worker-BSzbM0Uo.js → worker-VPLhQVba.js} +230 -230
- package/dist/assets/worker-b9bkZkXK.js +116 -0
- package/dist/assets/{worker-Cm3I3tnR.js → worker-k7HZ3t-V.js} +3 -20
- package/dist/index.html +2 -2
- package/dist/llm-streaming-server/chatOperations.js +19 -0
- package/dist/llm-streaming-server/llmStreaming.js +21 -14
- package/dist/server/aiChatHandler/index.js +6 -3
- package/package.json +23 -22
- package/src/llm-streaming-server/chatOperations.ts +27 -0
- package/src/llm-streaming-server/llmStreaming.ts +27 -14
- package/src/llm-streaming-ui/components/Message.tsx +8 -7
- package/src/llm-streaming-ui/components/MessageList.tsx +5 -0
- package/src/llm-streaming-ui/components/index.tsx +2 -2
- package/src/llm-streaming-ui/components/styles.scss +10 -0
- package/src/server/aiChatHandler/index.ts +9 -1
- package/src/types.ts +1 -0
- package/dist/assets/index-CfDs-dAu.js +0 -476
- package/dist/assets/worker-KiuLM-X4.js +0 -136
- package/dist/server/aiChatHandler/aiEditing.js +0 -58
- package/dist/server/aiChatHandler/chatOperations.js +0 -494
- package/dist/server/aiChatHandler/errorHandling.js +0 -49
- package/dist/server/aiChatHandler/llmStreaming.js +0 -265
- package/dist/server/aiChatHandler/validation.js +0 -19
- package/src/client/VZSidebar/AIChat/ChatInput.tsx +0 -237
- package/src/client/VZSidebar/AIChat/DiffView.scss +0 -173
- package/src/client/VZSidebar/AIChat/DiffView.tsx +0 -236
- package/src/client/VZSidebar/AIChat/FileEditingIndicator.tsx +0 -89
- package/src/client/VZSidebar/AIChat/IndividualFileDiff.tsx +0 -86
- package/src/client/VZSidebar/AIChat/JumpToLatestButton.tsx +0 -64
- package/src/client/VZSidebar/AIChat/Message.tsx +0 -145
- package/src/client/VZSidebar/AIChat/MessageList.tsx +0 -241
- package/src/client/VZSidebar/AIChat/ThinkingScratchpad.tsx +0 -42
- package/src/client/VZSidebar/AIChat/TypingIndicator.tsx +0 -19
- package/src/client/VZSidebar/AIChat/index.tsx +0 -388
- package/src/client/VZSidebar/AIChat/styles.scss +0 -831
- package/src/client/VZSidebar/AIChat/useSpeechRecognition.ts +0 -116
- package/src/server/aiChatHandler/aiEditing.ts +0 -102
- package/src/server/aiChatHandler/chatOperations.ts +0 -655
- package/src/server/aiChatHandler/errorHandling.ts +0 -66
- package/src/server/aiChatHandler/llmStreaming.ts +0 -403
- package/src/server/aiChatHandler/validation.ts +0 -24
|
@@ -1,265 +0,0 @@
|
|
|
1
|
-
import OpenAI from 'openai';
|
|
2
|
-
import { parseMarkdownFiles, StreamingMarkdownParser, } from 'llm-code-format';
|
|
3
|
-
import { mergeFileChanges } from 'editcodewithai';
|
|
4
|
-
import { updateFiles, updateAIScratchpad, createStreamingAIMessage, addStreamingEvent, updateStreamingStatus, finalizeStreamingMessage, } from './chatOperations.js';
|
|
5
|
-
import { formatFiles } from '../prettier.js';
|
|
6
|
-
// Verbose logs
|
|
7
|
-
const DEBUG = false;
|
|
8
|
-
// Useful for testing/debugging the streaming behavior
|
|
9
|
-
const slowMode = false;
|
|
10
|
-
// If the `EMIT_FIXTURES` variable is true,
|
|
11
|
-
// then an output file in the `test/fixtures` folder
|
|
12
|
-
// with the before and after file states for testing purposes.
|
|
13
|
-
// This feeds into tests in codemirror-ot.
|
|
14
|
-
const EMIT_FIXTURES = false;
|
|
15
|
-
/**
|
|
16
|
-
* Creates and configures the LLM function for streaming with reasoning tokens
|
|
17
|
-
*/
|
|
18
|
-
export const createLLMFunction = ({ shareDBDoc, chatId,
|
|
19
|
-
// Feature flag to enable/disable reasoning tokens.
|
|
20
|
-
// When false, reasoning tokens are not requested from the API
|
|
21
|
-
// and reasoning content is not processed in the streaming response.
|
|
22
|
-
enableReasoningTokens = false, model, aiRequestOptions, }) => {
|
|
23
|
-
return async (fullPrompt) => {
|
|
24
|
-
// Create OpenRouter client for reasoning token support
|
|
25
|
-
const openRouterClient = new OpenAI({
|
|
26
|
-
apiKey: process.env.VZCODE_EDIT_WITH_AI_API_KEY,
|
|
27
|
-
baseURL: process.env.VZCODE_EDIT_WITH_AI_BASE_URL ||
|
|
28
|
-
'https://openrouter.ai/api/v1',
|
|
29
|
-
defaultHeaders: {
|
|
30
|
-
'HTTP-Referer': 'https://vizhub.com',
|
|
31
|
-
'X-Title': 'VizHub',
|
|
32
|
-
},
|
|
33
|
-
});
|
|
34
|
-
let fullContent = '';
|
|
35
|
-
let generationId = '';
|
|
36
|
-
let currentEditingFileName = null;
|
|
37
|
-
let accumulatedTextChunk = '';
|
|
38
|
-
let currentFileContent = '';
|
|
39
|
-
// Create streaming AI message
|
|
40
|
-
createStreamingAIMessage(shareDBDoc, chatId);
|
|
41
|
-
// Set initial content generation status
|
|
42
|
-
updateStreamingStatus(shareDBDoc, chatId, 'Formulating a plan...');
|
|
43
|
-
// Helper to get original file content
|
|
44
|
-
const getOriginalFileContent = (fileName) => {
|
|
45
|
-
const files = shareDBDoc.data.files;
|
|
46
|
-
for (const file of Object.values(files)) {
|
|
47
|
-
if (file.name === fileName) {
|
|
48
|
-
return file.text || '';
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
return '';
|
|
52
|
-
};
|
|
53
|
-
// Helper to emit text chunk when accumulated
|
|
54
|
-
const emitTextChunk = async () => {
|
|
55
|
-
if (accumulatedTextChunk.trim()) {
|
|
56
|
-
DEBUG &&
|
|
57
|
-
console.log('LLMStreaming: Emitting text chunk:', accumulatedTextChunk.substring(0, 100) + '...');
|
|
58
|
-
await addStreamingEvent(shareDBDoc, chatId, {
|
|
59
|
-
type: 'text_chunk',
|
|
60
|
-
content: accumulatedTextChunk,
|
|
61
|
-
timestamp: Date.now(),
|
|
62
|
-
});
|
|
63
|
-
accumulatedTextChunk = '';
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
// Helper to complete file editing
|
|
67
|
-
const completeFileEditing = async (fileName) => {
|
|
68
|
-
if (fileName) {
|
|
69
|
-
DEBUG &&
|
|
70
|
-
console.log(`LLMStreaming: Completing file editing for ${fileName}`);
|
|
71
|
-
await addStreamingEvent(shareDBDoc, chatId, {
|
|
72
|
-
type: 'file_complete',
|
|
73
|
-
fileName,
|
|
74
|
-
beforeContent: getOriginalFileContent(fileName),
|
|
75
|
-
afterContent: currentFileContent,
|
|
76
|
-
timestamp: Date.now(),
|
|
77
|
-
});
|
|
78
|
-
currentFileContent = '';
|
|
79
|
-
}
|
|
80
|
-
};
|
|
81
|
-
// Define callbacks for streaming parser
|
|
82
|
-
const callbacks = {
|
|
83
|
-
onFileNameChange: async (fileName, format) => {
|
|
84
|
-
DEBUG &&
|
|
85
|
-
console.log(`LLMStreaming: File changed to: ${fileName} (${format})`);
|
|
86
|
-
// Emit any accumulated text chunk first
|
|
87
|
-
await emitTextChunk();
|
|
88
|
-
// Complete previous file if any
|
|
89
|
-
if (currentEditingFileName) {
|
|
90
|
-
await completeFileEditing(currentEditingFileName);
|
|
91
|
-
}
|
|
92
|
-
// Start new file
|
|
93
|
-
currentEditingFileName = fileName;
|
|
94
|
-
currentFileContent = '';
|
|
95
|
-
// Emit file start event
|
|
96
|
-
await addStreamingEvent(shareDBDoc, chatId, {
|
|
97
|
-
type: 'file_start',
|
|
98
|
-
fileName,
|
|
99
|
-
timestamp: Date.now(),
|
|
100
|
-
});
|
|
101
|
-
// Update status
|
|
102
|
-
updateStreamingStatus(shareDBDoc, chatId, `Editing ${fileName}...`);
|
|
103
|
-
},
|
|
104
|
-
onCodeLine: async (line) => {
|
|
105
|
-
DEBUG && console.log(`Code line: ${line}`);
|
|
106
|
-
// Accumulate code content for the current file
|
|
107
|
-
currentFileContent += line + '\n';
|
|
108
|
-
},
|
|
109
|
-
onNonCodeLine: async (line) => {
|
|
110
|
-
DEBUG && console.log(`Non-code line: ${line}`);
|
|
111
|
-
// Accumulate non-code content as text chunk
|
|
112
|
-
if (line.trim() !== '') {
|
|
113
|
-
accumulatedTextChunk += line + '\n';
|
|
114
|
-
// Update status for subsequent non-code chunks
|
|
115
|
-
if (firstNonCodeChunkProcessed) {
|
|
116
|
-
updateStreamingStatus(shareDBDoc, chatId, 'Describing changes...');
|
|
117
|
-
}
|
|
118
|
-
else {
|
|
119
|
-
firstNonCodeChunkProcessed = true;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
},
|
|
123
|
-
onFileDelete: async (fileName) => {
|
|
124
|
-
DEBUG &&
|
|
125
|
-
console.log(`LLMStreaming: File marked for deletion: ${fileName}`);
|
|
126
|
-
// Emit any accumulated text chunk first
|
|
127
|
-
await emitTextChunk();
|
|
128
|
-
// Complete previous file if any
|
|
129
|
-
if (currentEditingFileName) {
|
|
130
|
-
await completeFileEditing(currentEditingFileName);
|
|
131
|
-
}
|
|
132
|
-
// Reset current editing state
|
|
133
|
-
currentEditingFileName = null;
|
|
134
|
-
currentFileContent = '';
|
|
135
|
-
// Emit file delete event
|
|
136
|
-
await addStreamingEvent(shareDBDoc, chatId, {
|
|
137
|
-
type: 'file_delete',
|
|
138
|
-
fileName,
|
|
139
|
-
timestamp: Date.now(),
|
|
140
|
-
});
|
|
141
|
-
// Update status
|
|
142
|
-
updateStreamingStatus(shareDBDoc, chatId, `Deleting ${fileName}...`);
|
|
143
|
-
},
|
|
144
|
-
};
|
|
145
|
-
const parser = new StreamingMarkdownParser(callbacks);
|
|
146
|
-
const chunks = [];
|
|
147
|
-
let reasoningContent = '';
|
|
148
|
-
// Stream the response with reasoning tokens
|
|
149
|
-
const modelName = model ||
|
|
150
|
-
process.env.VZCODE_EDIT_WITH_AI_MODEL_NAME ||
|
|
151
|
-
'anthropic/claude-3.5-sonnet';
|
|
152
|
-
// Configure reasoning tokens based on enableReasoningTokens flag
|
|
153
|
-
const requestConfig = {
|
|
154
|
-
model: modelName,
|
|
155
|
-
messages: [{ role: 'user', content: fullPrompt }],
|
|
156
|
-
usage: { include: true },
|
|
157
|
-
stream: true,
|
|
158
|
-
...aiRequestOptions,
|
|
159
|
-
};
|
|
160
|
-
// Only include reasoning configuration if reasoning tokens are enabled
|
|
161
|
-
if (enableReasoningTokens) {
|
|
162
|
-
requestConfig.reasoning = {
|
|
163
|
-
effort: 'low',
|
|
164
|
-
exclude: false,
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
const stream = await openRouterClient.chat.completions.create(requestConfig);
|
|
168
|
-
let reasoningStarted = false;
|
|
169
|
-
let contentStarted = false;
|
|
170
|
-
let firstNonCodeChunkProcessed = false;
|
|
171
|
-
for await (const chunk of stream) {
|
|
172
|
-
if (slowMode) {
|
|
173
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
174
|
-
}
|
|
175
|
-
const delta = chunk.choices[0]?.delta; // Type assertion for OpenRouter-specific reasoning fields
|
|
176
|
-
if (delta?.reasoning && enableReasoningTokens) {
|
|
177
|
-
// Handle reasoning tokens (thinking) - only if enabled
|
|
178
|
-
if (!reasoningStarted) {
|
|
179
|
-
reasoningStarted = true;
|
|
180
|
-
updateStreamingStatus(shareDBDoc, chatId, 'Thinking...');
|
|
181
|
-
}
|
|
182
|
-
reasoningContent += delta.reasoning;
|
|
183
|
-
updateAIScratchpad(shareDBDoc, chatId, reasoningContent);
|
|
184
|
-
}
|
|
185
|
-
else if (delta?.content) {
|
|
186
|
-
// Handle regular content tokens
|
|
187
|
-
if (!contentStarted) {
|
|
188
|
-
contentStarted = true;
|
|
189
|
-
if (reasoningStarted) {
|
|
190
|
-
// Clear reasoning when content starts
|
|
191
|
-
updateAIScratchpad(shareDBDoc, chatId, '');
|
|
192
|
-
}
|
|
193
|
-
// // Set initial content generation status
|
|
194
|
-
// updateStreamingStatus(
|
|
195
|
-
// shareDBDoc,
|
|
196
|
-
// chatId,
|
|
197
|
-
// 'Formulating a plan...',
|
|
198
|
-
// );
|
|
199
|
-
}
|
|
200
|
-
const chunkContent = delta.content;
|
|
201
|
-
chunks.push(chunkContent);
|
|
202
|
-
await parser.processChunk(chunkContent);
|
|
203
|
-
fullContent += chunkContent;
|
|
204
|
-
}
|
|
205
|
-
else if (chunk.usage) {
|
|
206
|
-
// Handle usage information
|
|
207
|
-
DEBUG && console.log('Usage:', chunk.usage);
|
|
208
|
-
}
|
|
209
|
-
if (!generationId && chunk.id) {
|
|
210
|
-
generationId = chunk.id;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
await parser.flushRemaining();
|
|
214
|
-
// Emit any remaining text chunk
|
|
215
|
-
await emitTextChunk();
|
|
216
|
-
// Complete final file if any
|
|
217
|
-
if (currentEditingFileName) {
|
|
218
|
-
await completeFileEditing(currentEditingFileName);
|
|
219
|
-
}
|
|
220
|
-
// // Capture the current state of files before applying changes
|
|
221
|
-
// const beforeFiles = createFilesSnapshot(
|
|
222
|
-
// shareDBDoc.data.files,
|
|
223
|
-
// );
|
|
224
|
-
// Parse the full content to extract file changes
|
|
225
|
-
// export type FileCollection = Record<string, string>;
|
|
226
|
-
const newFilesUnformatted = parseMarkdownFiles(fullContent, 'bold').files;
|
|
227
|
-
// Run Prettier on `newFiles` before applying them,
|
|
228
|
-
// preserving empty files as empty
|
|
229
|
-
// since that is the cue to delete a file.
|
|
230
|
-
const newFilesFormatted = await formatFiles(newFilesUnformatted);
|
|
231
|
-
// Capture the current state of files before applying changes
|
|
232
|
-
let vizFilesBefore;
|
|
233
|
-
if (EMIT_FIXTURES) {
|
|
234
|
-
vizFilesBefore = JSON.parse(JSON.stringify(shareDBDoc.data.files));
|
|
235
|
-
}
|
|
236
|
-
// Apply all the edits at once
|
|
237
|
-
const vizFilesAfter = mergeFileChanges(shareDBDoc.data.files, newFilesFormatted);
|
|
238
|
-
const filesOp = updateFiles(shareDBDoc, vizFilesAfter);
|
|
239
|
-
if (EMIT_FIXTURES) {
|
|
240
|
-
const fs = await import('fs');
|
|
241
|
-
const path = await import('path');
|
|
242
|
-
const testCasesDir = path.resolve(process.cwd(), '../', 'fixtures');
|
|
243
|
-
if (!fs.existsSync(testCasesDir)) {
|
|
244
|
-
fs.mkdirSync(testCasesDir, { recursive: true });
|
|
245
|
-
}
|
|
246
|
-
const timestamp = new Date()
|
|
247
|
-
.toISOString()
|
|
248
|
-
.replace(/[:.]/g, '-');
|
|
249
|
-
const testCasePath = path.join(testCasesDir, `ai-chat-${timestamp}.json`);
|
|
250
|
-
const testCaseData = {
|
|
251
|
-
vizFilesBefore,
|
|
252
|
-
vizFilesAfter,
|
|
253
|
-
filesOp,
|
|
254
|
-
};
|
|
255
|
-
fs.writeFileSync(testCasePath, JSON.stringify(testCaseData, null, 2));
|
|
256
|
-
console.log(`AI chat test case written to ${testCasePath}`);
|
|
257
|
-
}
|
|
258
|
-
// Finalize streaming message
|
|
259
|
-
finalizeStreamingMessage(shareDBDoc, chatId);
|
|
260
|
-
return {
|
|
261
|
-
content: fullContent,
|
|
262
|
-
generationId: generationId,
|
|
263
|
-
};
|
|
264
|
-
};
|
|
265
|
-
};
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Validates incoming request data for AI chat messages
|
|
3
|
-
*/
|
|
4
|
-
export const validateRequest = (req, res) => {
|
|
5
|
-
const { content, chatId } = req.body;
|
|
6
|
-
if (!content || typeof content !== 'string') {
|
|
7
|
-
res.status(400).json({
|
|
8
|
-
error: 'Invalid request: content is required and must be a string',
|
|
9
|
-
});
|
|
10
|
-
return false;
|
|
11
|
-
}
|
|
12
|
-
if (!chatId || typeof chatId !== 'string') {
|
|
13
|
-
res.status(400).json({
|
|
14
|
-
error: 'Invalid request: chatId is required and must be a string',
|
|
15
|
-
});
|
|
16
|
-
return false;
|
|
17
|
-
}
|
|
18
|
-
return true;
|
|
19
|
-
};
|
|
@@ -1,237 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
useRef,
|
|
3
|
-
useEffect,
|
|
4
|
-
useCallback,
|
|
5
|
-
memo,
|
|
6
|
-
} from 'react';
|
|
7
|
-
import {
|
|
8
|
-
Form,
|
|
9
|
-
Button,
|
|
10
|
-
ButtonGroup,
|
|
11
|
-
ToggleButton,
|
|
12
|
-
} from '../../bootstrap';
|
|
13
|
-
import { enableAskMode } from '../../featureFlags';
|
|
14
|
-
import { useSpeechRecognition } from './useSpeechRecognition';
|
|
15
|
-
import { MicSVG, MicOffSVG } from '../../Icons';
|
|
16
|
-
|
|
17
|
-
interface ChatInputProps {
|
|
18
|
-
aiChatMessage: string;
|
|
19
|
-
setAIChatMessage: (message: string) => void;
|
|
20
|
-
onSendMessage: () => void;
|
|
21
|
-
focused: boolean;
|
|
22
|
-
aiChatMode: 'ask' | 'edit';
|
|
23
|
-
setAIChatMode: (mode: 'ask' | 'edit') => void;
|
|
24
|
-
navigateMessageHistoryUp: () => void;
|
|
25
|
-
navigateMessageHistoryDown: () => void;
|
|
26
|
-
resetMessageHistoryNavigation: () => void;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const ChatInputComponent = ({
|
|
30
|
-
aiChatMessage,
|
|
31
|
-
setAIChatMessage,
|
|
32
|
-
onSendMessage,
|
|
33
|
-
focused,
|
|
34
|
-
aiChatMode,
|
|
35
|
-
setAIChatMode,
|
|
36
|
-
navigateMessageHistoryUp,
|
|
37
|
-
navigateMessageHistoryDown,
|
|
38
|
-
resetMessageHistoryNavigation,
|
|
39
|
-
}: ChatInputProps) => {
|
|
40
|
-
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
41
|
-
|
|
42
|
-
// Use the speech recognition hook
|
|
43
|
-
const {
|
|
44
|
-
isSpeaking,
|
|
45
|
-
toggleSpeechRecognition,
|
|
46
|
-
stopSpeaking,
|
|
47
|
-
} = useSpeechRecognition(setAIChatMessage);
|
|
48
|
-
|
|
49
|
-
useEffect(() => {
|
|
50
|
-
// Focus the input when the AI chat is focused
|
|
51
|
-
if (focused && inputRef.current) {
|
|
52
|
-
inputRef.current.focus();
|
|
53
|
-
}
|
|
54
|
-
}, [focused]);
|
|
55
|
-
|
|
56
|
-
const handleKeyDown = useCallback(
|
|
57
|
-
(event: React.KeyboardEvent) => {
|
|
58
|
-
if (event.key === 'Enter' && !event.shiftKey) {
|
|
59
|
-
event.preventDefault();
|
|
60
|
-
onSendMessage();
|
|
61
|
-
} else if (event.key === 'Enter' && event.shiftKey) {
|
|
62
|
-
// Shift+Enter should add a newline, not trigger run code
|
|
63
|
-
// We prevent the event from bubbling up to the global keyboard handler
|
|
64
|
-
event.stopPropagation();
|
|
65
|
-
} else if (event.key === 'ArrowUp') {
|
|
66
|
-
// Only navigate history if cursor is at the beginning of the first line
|
|
67
|
-
const textarea =
|
|
68
|
-
event.target as HTMLTextAreaElement;
|
|
69
|
-
const { selectionStart, value } = textarea;
|
|
70
|
-
const lines = value
|
|
71
|
-
.substring(0, selectionStart)
|
|
72
|
-
.split('\n');
|
|
73
|
-
|
|
74
|
-
// Check if we're at the beginning of the first line
|
|
75
|
-
if (lines.length === 1 && selectionStart === 0) {
|
|
76
|
-
event.preventDefault();
|
|
77
|
-
navigateMessageHistoryUp();
|
|
78
|
-
}
|
|
79
|
-
} else if (event.key === 'ArrowDown') {
|
|
80
|
-
// Only navigate history if cursor is at the end of the last line
|
|
81
|
-
const textarea =
|
|
82
|
-
event.target as HTMLTextAreaElement;
|
|
83
|
-
const { selectionStart, value } = textarea;
|
|
84
|
-
const remainingText =
|
|
85
|
-
value.substring(selectionStart);
|
|
86
|
-
const remainingLines = remainingText.split('\n');
|
|
87
|
-
|
|
88
|
-
// Check if we're at the end of the last line
|
|
89
|
-
if (
|
|
90
|
-
remainingLines.length === 1 &&
|
|
91
|
-
remainingLines[0] === ''
|
|
92
|
-
) {
|
|
93
|
-
event.preventDefault();
|
|
94
|
-
navigateMessageHistoryDown();
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
},
|
|
98
|
-
[
|
|
99
|
-
onSendMessage,
|
|
100
|
-
navigateMessageHistoryUp,
|
|
101
|
-
navigateMessageHistoryDown,
|
|
102
|
-
],
|
|
103
|
-
);
|
|
104
|
-
|
|
105
|
-
const handleSendClick = useCallback(() => {
|
|
106
|
-
// If speech recognition is active, stop it
|
|
107
|
-
if (isSpeaking) {
|
|
108
|
-
stopSpeaking();
|
|
109
|
-
}
|
|
110
|
-
onSendMessage();
|
|
111
|
-
}, [onSendMessage, isSpeaking, stopSpeaking]);
|
|
112
|
-
|
|
113
|
-
const handleChange = useCallback(
|
|
114
|
-
(event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
115
|
-
setAIChatMessage(event.target.value);
|
|
116
|
-
// Reset history navigation when user starts typing
|
|
117
|
-
resetMessageHistoryNavigation();
|
|
118
|
-
},
|
|
119
|
-
[setAIChatMessage, resetMessageHistoryNavigation],
|
|
120
|
-
);
|
|
121
|
-
|
|
122
|
-
return (
|
|
123
|
-
<div className="ai-chat-input-container">
|
|
124
|
-
{enableAskMode && (
|
|
125
|
-
<div
|
|
126
|
-
className="ai-chat-mode-toggle"
|
|
127
|
-
style={{ marginBottom: '8px' }}
|
|
128
|
-
>
|
|
129
|
-
<ButtonGroup size="sm">
|
|
130
|
-
<ToggleButton
|
|
131
|
-
id="ai-chat-mode-ask"
|
|
132
|
-
type="radio"
|
|
133
|
-
variant={
|
|
134
|
-
aiChatMode === 'ask'
|
|
135
|
-
? 'primary'
|
|
136
|
-
: 'outline-primary'
|
|
137
|
-
}
|
|
138
|
-
name="ai-chat-mode"
|
|
139
|
-
value="ask"
|
|
140
|
-
checked={aiChatMode === 'ask'}
|
|
141
|
-
onChange={() => setAIChatMode('ask')}
|
|
142
|
-
>
|
|
143
|
-
💬 Ask
|
|
144
|
-
</ToggleButton>
|
|
145
|
-
<ToggleButton
|
|
146
|
-
id="ai-chat-mode-edit"
|
|
147
|
-
type="radio"
|
|
148
|
-
variant={
|
|
149
|
-
aiChatMode === 'edit'
|
|
150
|
-
? 'primary'
|
|
151
|
-
: 'outline-primary'
|
|
152
|
-
}
|
|
153
|
-
name="ai-chat-mode"
|
|
154
|
-
value="edit"
|
|
155
|
-
checked={aiChatMode === 'edit'}
|
|
156
|
-
onChange={() => setAIChatMode('edit')}
|
|
157
|
-
>
|
|
158
|
-
✏️ Edit
|
|
159
|
-
</ToggleButton>
|
|
160
|
-
</ButtonGroup>
|
|
161
|
-
<div className="ai-chat-mode-description">
|
|
162
|
-
{aiChatMode === 'ask'
|
|
163
|
-
? 'Ask questions without editing files'
|
|
164
|
-
: 'Get answers and code edits'}
|
|
165
|
-
</div>
|
|
166
|
-
</div>
|
|
167
|
-
)}
|
|
168
|
-
<Form.Group className="ai-chat-input-group">
|
|
169
|
-
<Form.Control
|
|
170
|
-
as="textarea"
|
|
171
|
-
rows={10}
|
|
172
|
-
value={aiChatMessage}
|
|
173
|
-
onChange={handleChange}
|
|
174
|
-
onKeyDown={handleKeyDown}
|
|
175
|
-
ref={inputRef}
|
|
176
|
-
placeholder={
|
|
177
|
-
aiChatMode === 'edit'
|
|
178
|
-
? 'Ask me to make code changes...'
|
|
179
|
-
: 'Ask me anything about your code...'
|
|
180
|
-
}
|
|
181
|
-
spellCheck="false"
|
|
182
|
-
aria-label="Chat message input"
|
|
183
|
-
/>
|
|
184
|
-
<div className="ai-chat-input-footer">
|
|
185
|
-
<span className="ai-chat-hint">
|
|
186
|
-
{aiChatMessage ? 'Press Enter to send' : ''}
|
|
187
|
-
</span>
|
|
188
|
-
<div style={{ display: 'flex', gap: '8px' }}>
|
|
189
|
-
<Button
|
|
190
|
-
variant={
|
|
191
|
-
isSpeaking ? 'danger' : 'outline-secondary'
|
|
192
|
-
}
|
|
193
|
-
size="sm"
|
|
194
|
-
onClick={toggleSpeechRecognition}
|
|
195
|
-
aria-label={
|
|
196
|
-
isSpeaking
|
|
197
|
-
? 'Stop voice typing'
|
|
198
|
-
: 'Start voice typing'
|
|
199
|
-
}
|
|
200
|
-
title={
|
|
201
|
-
isSpeaking
|
|
202
|
-
? 'Stop voice typing'
|
|
203
|
-
: 'Start voice typing'
|
|
204
|
-
}
|
|
205
|
-
style={{
|
|
206
|
-
borderColor: isSpeaking
|
|
207
|
-
? undefined
|
|
208
|
-
: '#dee2e6',
|
|
209
|
-
borderRadius: '0.375rem',
|
|
210
|
-
borderWidth: '1px',
|
|
211
|
-
borderStyle: 'solid',
|
|
212
|
-
}}
|
|
213
|
-
>
|
|
214
|
-
{isSpeaking ? <MicOffSVG /> : <MicSVG />}
|
|
215
|
-
</Button>
|
|
216
|
-
<Button
|
|
217
|
-
variant={
|
|
218
|
-
aiChatMessage.trim()
|
|
219
|
-
? 'primary'
|
|
220
|
-
: 'outline-secondary'
|
|
221
|
-
}
|
|
222
|
-
onClick={handleSendClick}
|
|
223
|
-
disabled={!aiChatMessage.trim()}
|
|
224
|
-
className="ai-chat-send-button"
|
|
225
|
-
aria-label="Send message"
|
|
226
|
-
title="Send message (Enter)"
|
|
227
|
-
>
|
|
228
|
-
Send
|
|
229
|
-
</Button>
|
|
230
|
-
</div>
|
|
231
|
-
</div>
|
|
232
|
-
</Form.Group>
|
|
233
|
-
</div>
|
|
234
|
-
);
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
export const ChatInput = memo(ChatInputComponent);
|
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
.diff-view {
|
|
2
|
-
margin: 12px 0;
|
|
3
|
-
|
|
4
|
-
.diff-summary {
|
|
5
|
-
margin-bottom: 8px;
|
|
6
|
-
display: flex;
|
|
7
|
-
justify-content: space-between;
|
|
8
|
-
align-items: center;
|
|
9
|
-
|
|
10
|
-
.diff-stats {
|
|
11
|
-
display: flex;
|
|
12
|
-
align-items: center;
|
|
13
|
-
gap: 8px;
|
|
14
|
-
font-size: 14px;
|
|
15
|
-
color: var(--bs-secondary-color);
|
|
16
|
-
|
|
17
|
-
.files-changed {
|
|
18
|
-
color: white;
|
|
19
|
-
font-weight: 600;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
.additions {
|
|
23
|
-
color: #28a745;
|
|
24
|
-
font-weight: 600;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
.deletions {
|
|
28
|
-
color: #dc3545;
|
|
29
|
-
font-weight: 600;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
.diff-files {
|
|
35
|
-
display: flex;
|
|
36
|
-
flex-direction: column;
|
|
37
|
-
gap: 4px;
|
|
38
|
-
|
|
39
|
-
// Fix diff2html styling issues for dark theme and layout
|
|
40
|
-
.d2h-wrapper {
|
|
41
|
-
// Override diff2html variables for dark theme
|
|
42
|
-
--d2h-bg-color: rgb(41, 44, 52);
|
|
43
|
-
--d2h-color: #e6edf3;
|
|
44
|
-
--d2h-border-color: #30363d;
|
|
45
|
-
--d2h-dim-color: #6e7681;
|
|
46
|
-
--d2h-info-bg-color: #161b22;
|
|
47
|
-
--d2h-file-header-bg-color: #161b22;
|
|
48
|
-
--d2h-file-header-border-color: #30363d;
|
|
49
|
-
--d2h-line-border-color: #21262d;
|
|
50
|
-
--d2h-ins-bg-color: rgba(46, 160, 67, 0.15);
|
|
51
|
-
--d2h-ins-border-color: rgba(46, 160, 67, 0.4);
|
|
52
|
-
--d2h-ins-highlight-bg-color: rgba(46, 160, 67, 0.4);
|
|
53
|
-
--d2h-ins-label-color: #3fb950;
|
|
54
|
-
--d2h-del-bg-color: rgba(248, 81, 73, 0.1);
|
|
55
|
-
--d2h-del-border-color: rgba(248, 81, 73, 0.4);
|
|
56
|
-
--d2h-del-highlight-bg-color: rgba(248, 81, 73, 0.4);
|
|
57
|
-
--d2h-del-label-color: #f85149;
|
|
58
|
-
|
|
59
|
-
background-color: var(--d2h-bg-color);
|
|
60
|
-
color: var(--d2h-color);
|
|
61
|
-
border-radius: 6px;
|
|
62
|
-
overflow: hidden;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Fix layout issues with line numbers
|
|
66
|
-
.d2h-diff-table {
|
|
67
|
-
position: relative;
|
|
68
|
-
background-color: var(--d2h-bg-color);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
.d2h-code-linenumber {
|
|
72
|
-
position: relative !important;
|
|
73
|
-
float: none !important;
|
|
74
|
-
display: table-cell !important;
|
|
75
|
-
width: auto !important;
|
|
76
|
-
background-color: var(--d2h-bg-color) !important;
|
|
77
|
-
border-color: var(--d2h-line-border-color) !important;
|
|
78
|
-
color: var(--d2h-dim-color) !important;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
.d2h-code-side-linenumber {
|
|
82
|
-
position: relative !important;
|
|
83
|
-
float: none !important;
|
|
84
|
-
display: table-cell !important;
|
|
85
|
-
width: auto !important;
|
|
86
|
-
background-color: var(--d2h-bg-color) !important;
|
|
87
|
-
border-color: var(--d2h-line-border-color) !important;
|
|
88
|
-
color: var(--d2h-dim-color) !important;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Ensure proper table layout
|
|
92
|
-
.d2h-diff-tbody tr {
|
|
93
|
-
display: table-row;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
.d2h-diff-tbody td {
|
|
97
|
-
display: table-cell;
|
|
98
|
-
vertical-align: top;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Fix file header styling
|
|
102
|
-
.d2h-file-header {
|
|
103
|
-
display: none !important;
|
|
104
|
-
background-color: var(
|
|
105
|
-
--d2h-file-header-bg-color
|
|
106
|
-
) !important;
|
|
107
|
-
border-bottom: 1px solid
|
|
108
|
-
var(--d2h-file-header-border-color) !important;
|
|
109
|
-
color: var(--d2h-color);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Ensure proper line styling
|
|
113
|
-
.d2h-del {
|
|
114
|
-
background-color: var(--d2h-del-bg-color) !important;
|
|
115
|
-
border-color: var(--d2h-del-border-color) !important;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
.d2h-ins {
|
|
119
|
-
background-color: var(--d2h-ins-bg-color) !important;
|
|
120
|
-
border-color: var(--d2h-ins-border-color) !important;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Style clickable file names
|
|
124
|
-
.d2h-file-name {
|
|
125
|
-
&:hover {
|
|
126
|
-
text-decoration: underline !important;
|
|
127
|
-
opacity: 0.8;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Fix wrapper border
|
|
132
|
-
.d2h-file-wrapper {
|
|
133
|
-
border: 1px solid var(--d2h-border-color) !important;
|
|
134
|
-
background-color: var(--d2h-bg-color);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
.deleted-file {
|
|
139
|
-
margin-bottom: 8px;
|
|
140
|
-
border: 1px solid #30363d;
|
|
141
|
-
border-radius: 6px;
|
|
142
|
-
background-color: rgb(41, 44, 52);
|
|
143
|
-
overflow: hidden;
|
|
144
|
-
|
|
145
|
-
.deleted-file-header {
|
|
146
|
-
display: flex;
|
|
147
|
-
justify-content: space-between;
|
|
148
|
-
align-items: center;
|
|
149
|
-
padding: 12px 16px;
|
|
150
|
-
background-color: #161b22;
|
|
151
|
-
border-bottom: 1px solid #30363d;
|
|
152
|
-
|
|
153
|
-
.deleted-file-name {
|
|
154
|
-
color: #e6edf3;
|
|
155
|
-
font-weight: 600;
|
|
156
|
-
font-family:
|
|
157
|
-
'SFMono-Regular', Consolas, 'Liberation Mono',
|
|
158
|
-
Menlo, monospace;
|
|
159
|
-
font-size: 14px;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
.deleted-file-status {
|
|
163
|
-
color: #f85149;
|
|
164
|
-
font-weight: 600;
|
|
165
|
-
font-size: 14px;
|
|
166
|
-
background-color: rgba(248, 81, 73, 0.1);
|
|
167
|
-
padding: 4px 8px;
|
|
168
|
-
border-radius: 4px;
|
|
169
|
-
border: 1px solid rgba(248, 81, 73, 0.4);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
}
|