vzcode 2.3.0 → 2.4.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/package.json +3 -2
- package/src/cli.ts +208 -0
- package/src/client/AIAssist/AIAssistCodeMirrorKeyMap.ts +42 -0
- package/src/client/AIAssist/AIAssistWidget/index.tsx +135 -0
- package/src/client/AIAssist/AIAssistWidget/style.scss +47 -0
- package/src/client/AIAssist/Spinner.jsx +76 -0
- package/src/client/AIAssist/formatFile.ts +27 -0
- package/src/client/AIAssist/generateFilesContext.ts +25 -0
- package/src/client/AIAssist/index.ts +2 -0
- package/src/client/AIAssist/startAIAssist.ts +95 -0
- package/src/client/App/index.tsx +152 -0
- package/src/client/App/style.scss +82 -0
- package/src/client/App/usePending.ts +51 -0
- package/src/client/App/useShareDB.ts +161 -0
- package/src/client/CodeEditor/Copilot/index.ts +74 -0
- package/src/client/CodeEditor/InteractiveWidgets/boolToggler.ts +12 -0
- package/src/client/CodeEditor/InteractiveWidgets/colorPicker.ts +68 -0
- package/src/client/CodeEditor/InteractiveWidgets/colorsInTextPlugin.ts +174 -0
- package/src/client/CodeEditor/InteractiveWidgets/highlightWidgets.ts +79 -0
- package/src/client/CodeEditor/InteractiveWidgets/index.ts +44 -0
- package/src/client/CodeEditor/InteractiveWidgets/markdownCheckboxToggler.ts +12 -0
- package/src/client/CodeEditor/InteractiveWidgets/numberDragger.ts +20 -0
- package/src/client/CodeEditor/InteractiveWidgets/rotateWidget.ts +143 -0
- package/src/client/CodeEditor/InteractiveWidgets/urlClicker.test.ts +48 -0
- package/src/client/CodeEditor/InteractiveWidgets/urlClicker.ts +12 -0
- package/src/client/CodeEditor/InteractiveWidgets/vec2Slider.ts +25 -0
- package/src/client/CodeEditor/basicSetup.ts +73 -0
- package/src/client/CodeEditor/getOrCreateEditor.tsx +615 -0
- package/src/client/CodeEditor/index.tsx +202 -0
- package/src/client/CodeEditor/json1PresenceBroadcast.ts +41 -0
- package/src/client/CodeEditor/json1PresenceDisplay.ts +350 -0
- package/src/client/CodeEditor/rainbowBrackets.ts +122 -0
- package/src/client/CodeEditor/style.scss +156 -0
- package/src/client/CodeEditor/typescriptExtension/worker.ts +32 -0
- package/src/client/CodeErrorOverlay/index.tsx +77 -0
- package/src/client/CodeErrorOverlay/style.scss +33 -0
- package/src/client/Fonts/fonts.ts +8 -0
- package/src/client/Icons/AdjustmentSVG.tsx +19 -0
- package/src/client/Icons/BugSVG.tsx +20 -0
- package/src/client/Icons/CloseSVG.tsx +19 -0
- package/src/client/Icons/CssSVG.tsx +16 -0
- package/src/client/Icons/DirectoryArrowSVG.tsx +19 -0
- package/src/client/Icons/EditSVG.tsx +19 -0
- package/src/client/Icons/FileSVG.tsx +19 -0
- package/src/client/Icons/FolderSVG.tsx +20 -0
- package/src/client/Icons/GearSVG.tsx +16 -0
- package/src/client/Icons/HtmlSVG.tsx +16 -0
- package/src/client/Icons/JavaScriptSVG.tsx +16 -0
- package/src/client/Icons/JsonSVG.tsx +16 -0
- package/src/client/Icons/MarkdownSVG.tsx +16 -0
- package/src/client/Icons/MicOffSVG.tsx +35 -0
- package/src/client/Icons/MicSVG.tsx +25 -0
- package/src/client/Icons/NewSVG.tsx +19 -0
- package/src/client/Icons/PinSVG.tsx +14 -0
- package/src/client/Icons/PlaySVG.tsx +16 -0
- package/src/client/Icons/QuestionMarkSVG.tsx +18 -0
- package/src/client/Icons/ReactSVG.tsx +16 -0
- package/src/client/Icons/SearchSVG.tsx +20 -0
- package/src/client/Icons/SparklesSVG.tsx +23 -0
- package/src/client/Icons/SplitEditorSVG.tsx +34 -0
- package/src/client/Icons/StopSVG.tsx +15 -0
- package/src/client/Icons/SvelteSVG.tsx +16 -0
- package/src/client/Icons/TrashSVG.tsx +19 -0
- package/src/client/Icons/TypeScriptSVG.tsx +16 -0
- package/src/client/Icons/index.tsx +26 -0
- package/src/client/ImageViewer/index.tsx +91 -0
- package/src/client/ImageViewer/style.scss +41 -0
- package/src/client/PresenceNotifications/PresenceNotifications.test.ts +134 -0
- package/src/client/PresenceNotifications/index.tsx +158 -0
- package/src/client/PresenceNotifications/style.scss +17 -0
- package/src/client/RunCodeWidget/index.tsx +117 -0
- package/src/client/RunCodeWidget/style.scss +24 -0
- package/src/client/SplitPaneResizeContext.tsx +159 -0
- package/src/client/TabList/Tab.tsx +73 -0
- package/src/client/TabList/index.tsx +35 -0
- package/src/client/TabList/style.scss +51 -0
- package/src/client/VZCodeContext/index.tsx +29 -0
- package/src/client/VZCodeContext/types.ts +248 -0
- package/src/client/VZCodeContext/useVZCodeState.ts +602 -0
- package/src/client/VZKeyboardShortcutsDoc.tsx +148 -0
- package/src/client/VZLeft.tsx +28 -0
- package/src/client/VZMiddle.tsx +172 -0
- package/src/client/VZResizer/index.tsx +128 -0
- package/src/client/VZResizer/styles.scss +33 -0
- package/src/client/VZRight.tsx +113 -0
- package/src/client/VZSettings.tsx +275 -0
- package/src/client/VZSidebar/AIChat/ChatInput.tsx +243 -0
- package/src/client/VZSidebar/AIChat/DiffView.scss +136 -0
- package/src/client/VZSidebar/AIChat/DiffView.tsx +176 -0
- package/src/client/VZSidebar/AIChat/Message.tsx +79 -0
- package/src/client/VZSidebar/AIChat/MessageList.tsx +190 -0
- package/src/client/VZSidebar/AIChat/StreamingMessage.tsx +35 -0
- package/src/client/VZSidebar/AIChat/ThinkingScratchpad.tsx +156 -0
- package/src/client/VZSidebar/AIChat/TypingIndicator.tsx +19 -0
- package/src/client/VZSidebar/AIChat/index.tsx +357 -0
- package/src/client/VZSidebar/AIChat/styles.scss +646 -0
- package/src/client/VZSidebar/AIChat/useSpeechRecognition.ts +117 -0
- package/src/client/VZSidebar/CreateDirModal.tsx +111 -0
- package/src/client/VZSidebar/CreateFileModal.tsx +111 -0
- package/src/client/VZSidebar/DeleteConfirmationModal.tsx +76 -0
- package/src/client/VZSidebar/DirectoryListing.tsx +89 -0
- package/src/client/VZSidebar/EmptyState.tsx +11 -0
- package/src/client/VZSidebar/FileListing.tsx +91 -0
- package/src/client/VZSidebar/FileTypeIcon.tsx +49 -0
- package/src/client/VZSidebar/Item.tsx +237 -0
- package/src/client/VZSidebar/Listing.tsx +47 -0
- package/src/client/VZSidebar/Search.tsx +527 -0
- package/src/client/VZSidebar/VisualEditor.tsx +282 -0
- package/src/client/VZSidebar/VoiceChatModal.tsx +121 -0
- package/src/client/VZSidebar/aiCopyPaste.ts +199 -0
- package/src/client/VZSidebar/index.tsx +728 -0
- package/src/client/VZSidebar/styles.scss +612 -0
- package/src/client/VZSidebar/useDragAndDrop.tsx +408 -0
- package/src/client/bootstrap.ts +26 -0
- package/src/client/buildWorker.ts +3 -0
- package/src/client/featureFlags.ts +8 -0
- package/src/client/generateRequestId.ts +6 -0
- package/src/client/getFileTree.ts +53 -0
- package/src/client/index.ts +19 -0
- package/src/client/main.jsx +10 -0
- package/src/client/presenceColor.ts +8 -0
- package/src/client/shouldTriggerRun.ts +31 -0
- package/src/client/sortFileTree.ts +21 -0
- package/src/client/tabsSearchParameters.test.ts +79 -0
- package/src/client/tabsSearchParameters.ts +137 -0
- package/src/client/themes/index.ts +7 -0
- package/src/client/themes/themes.ts +77 -0
- package/src/client/themes/uiwImports.ts +32 -0
- package/src/client/themes/useDynamicTheme.ts +29 -0
- package/src/client/themes/vizhubTheme/colors.ts +80 -0
- package/src/client/themes/vizhubTheme/index.ts +193 -0
- package/src/client/useActions.ts +288 -0
- package/src/client/useESLint/index.ts +72 -0
- package/src/client/useESLint/worker.ts +113 -0
- package/src/client/useEditorCache.ts +35 -0
- package/src/client/useFileCRUD.ts +228 -0
- package/src/client/useKeyboardShortcuts.ts +451 -0
- package/src/client/useOpenDirectories.ts +99 -0
- package/src/client/usePresenceAutoFollow.ts +85 -0
- package/src/client/usePrettier/index.ts +178 -0
- package/src/client/usePrettier/worker.ts +109 -0
- package/src/client/useRunCode.tsx +15 -0
- package/src/client/useRuntimeError.ts +32 -0
- package/src/client/useSubmitOperation.ts +15 -0
- package/src/client/useURLSync.ts +137 -0
- package/src/client/usernameLocalStorage.ts +43 -0
- package/src/client/utils/fileExtension.ts +10 -0
- package/src/client/utils/isImageFile.ts +12 -0
- package/src/client/vzReducer/aiChatReducer.ts +44 -0
- package/src/client/vzReducer/closeTabReducer.test.ts +205 -0
- package/src/client/vzReducer/closeTabsReducer.ts +96 -0
- package/src/client/vzReducer/createInitialState.ts +38 -0
- package/src/client/vzReducer/editorNoLongerWantsFocusReducer.ts +9 -0
- package/src/client/vzReducer/findPane.ts +19 -0
- package/src/client/vzReducer/index.ts +250 -0
- package/src/client/vzReducer/openTabReducer.test.ts +127 -0
- package/src/client/vzReducer/openTabReducer.ts +74 -0
- package/src/client/vzReducer/searchReducer.test.ts +292 -0
- package/src/client/vzReducer/searchReducer.ts +181 -0
- package/src/client/vzReducer/setActiveFileIdReducer.ts +28 -0
- package/src/client/vzReducer/setActiveFileLeftRightReducer.ts +76 -0
- package/src/client/vzReducer/setIsDocOpenReducer.ts +9 -0
- package/src/client/vzReducer/setIsSettingsOpenReducer.ts +9 -0
- package/src/client/vzReducer/setThemeReducer.ts +9 -0
- package/src/client/vzReducer/setUsernameReducer.ts +9 -0
- package/src/client/vzReducer/splitCurrentPaneReducer.ts +66 -0
- package/src/client/vzReducer/toggleAutoFollowReducer.ts +12 -0
- package/src/client/vzReducer/updatePane.ts +40 -0
- package/src/client/vzReducer/updatePresenceIndicatorReducer.ts +30 -0
- package/src/client/vzReducer/visualEditorReducer.ts +9 -0
- package/src/ot.ts +24 -0
- package/src/randomId.ts +15 -0
- package/src/reactRouterExports.ts +15 -0
- package/src/runCode.ts +27 -0
- package/src/server/aiChatHandler/aiEditing.ts +151 -0
- package/src/server/aiChatHandler/chatOperations.ts +476 -0
- package/src/server/aiChatHandler/errorHandling.ts +66 -0
- package/src/server/aiChatHandler/index.ts +199 -0
- package/src/server/aiChatHandler/llmStreaming.ts +419 -0
- package/src/server/aiChatHandler/validation.ts +24 -0
- package/src/server/computeInitialDocument.ts +220 -0
- package/src/server/config.ts +24 -0
- package/src/server/featureFlags.ts +8 -0
- package/src/server/generateAIResponse.ts +188 -0
- package/src/server/handleAIAssist.ts +53 -0
- package/src/server/handleAIChatMessage.ts +2 -0
- package/src/server/handleAICopilot.ts +97 -0
- package/src/server/index.ts +379 -0
- package/src/server/isDirectory.ts +1 -0
- package/src/server/livekit.ts +24 -0
- package/src/server/prettier.ts +115 -0
- package/src/server/setupEnv.ts +8 -0
- package/src/submitOperation.ts +25 -0
- package/src/types.ts +205 -0
- package/src/utils/fileDiff.ts +117 -0
- package/src/vite-env.d.ts +10 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
useContext,
|
|
3
|
+
useEffect,
|
|
4
|
+
useMemo,
|
|
5
|
+
useRef,
|
|
6
|
+
} from 'react';
|
|
7
|
+
import {
|
|
8
|
+
UnifiedFilesDiff,
|
|
9
|
+
parseUnifiedDiffStats,
|
|
10
|
+
combineUnifiedDiffs,
|
|
11
|
+
} from '../../../utils/fileDiff';
|
|
12
|
+
import * as Diff2Html from 'diff2html';
|
|
13
|
+
import 'diff2html/bundles/css/diff2html.min.css';
|
|
14
|
+
import './DiffView.scss';
|
|
15
|
+
import { VZCodeContext } from '../../VZCodeContext';
|
|
16
|
+
import { VizFileId, VizFiles } from '@vizhub/viz-types';
|
|
17
|
+
|
|
18
|
+
interface DiffViewProps {
|
|
19
|
+
diffData: UnifiedFilesDiff;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const DiffView: React.FC<DiffViewProps> = ({
|
|
23
|
+
diffData,
|
|
24
|
+
}) => {
|
|
25
|
+
const { content, openTab, setIsAIChatOpen } =
|
|
26
|
+
useContext(VZCodeContext);
|
|
27
|
+
const diffContainerRef = useRef<HTMLDivElement>(null);
|
|
28
|
+
|
|
29
|
+
const unifiedDiffs = Object.values(diffData).filter(
|
|
30
|
+
(diff) => diff.length > 0,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
if (unifiedDiffs.length === 0) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Calculate statistics from all unified diffs
|
|
38
|
+
let totalAdditions = 0;
|
|
39
|
+
let totalDeletions = 0;
|
|
40
|
+
|
|
41
|
+
for (const unifiedDiff of unifiedDiffs) {
|
|
42
|
+
const stats = parseUnifiedDiffStats(unifiedDiff);
|
|
43
|
+
totalAdditions += stats.additions;
|
|
44
|
+
totalDeletions += stats.deletions;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Combine all unified diffs and convert to HTML using diff2html
|
|
48
|
+
const combinedUnifiedDiff = combineUnifiedDiffs(diffData);
|
|
49
|
+
const diffHtml = Diff2Html.html(combinedUnifiedDiff, {
|
|
50
|
+
drawFileList: false,
|
|
51
|
+
matching: 'words',
|
|
52
|
+
diffStyle: 'word',
|
|
53
|
+
outputFormat: 'line-by-line',
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Create a mapping from file name to file ID for click handling
|
|
57
|
+
const fileNameToIdMap = useMemo(() => {
|
|
58
|
+
const map = new Map<string, VizFileId>();
|
|
59
|
+
Object.entries(diffData).forEach(([fileId, diff]) => {
|
|
60
|
+
// Extract file name from the unified diff
|
|
61
|
+
// The diff contains lines like "--- a/filename" and "+++ b/filename"
|
|
62
|
+
const lines = diff.split('\n');
|
|
63
|
+
for (const line of lines) {
|
|
64
|
+
if (
|
|
65
|
+
line.startsWith('--- a/') ||
|
|
66
|
+
line.startsWith('+++ b/')
|
|
67
|
+
) {
|
|
68
|
+
const fileName = line.substring(6); // Remove "--- a/" or "+++ b/"
|
|
69
|
+
map.set(fileName, fileId as VizFileId);
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
return map;
|
|
75
|
+
}, [diffData]);
|
|
76
|
+
|
|
77
|
+
// Add click handlers to file names after HTML is rendered
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (!diffContainerRef.current) return;
|
|
80
|
+
|
|
81
|
+
const handleFileNameClick = (event: MouseEvent) => {
|
|
82
|
+
const target = event.target as HTMLElement;
|
|
83
|
+
if (target.classList.contains('d2h-file-name')) {
|
|
84
|
+
event.preventDefault();
|
|
85
|
+
event.stopPropagation();
|
|
86
|
+
|
|
87
|
+
console.log('File name clicked');
|
|
88
|
+
|
|
89
|
+
const fileName = target.textContent?.trim();
|
|
90
|
+
|
|
91
|
+
console.log('Clicked file name:', fileName);
|
|
92
|
+
if (fileName) {
|
|
93
|
+
const files: VizFiles = content?.files;
|
|
94
|
+
if (files) {
|
|
95
|
+
// export type VizFiles = {
|
|
96
|
+
// [fileId: VizFileId]: VizFile;
|
|
97
|
+
// };
|
|
98
|
+
// export type VizFileId = string;
|
|
99
|
+
// export type VizFile = {
|
|
100
|
+
// name: string;
|
|
101
|
+
// text: string;
|
|
102
|
+
// };
|
|
103
|
+
|
|
104
|
+
// TODO get fileId from content.files
|
|
105
|
+
const fileId = Object.entries(files).find(
|
|
106
|
+
([id, file]) => file.name === fileName,
|
|
107
|
+
)?.[0];
|
|
108
|
+
console.log('Mapped file ID:', fileId);
|
|
109
|
+
|
|
110
|
+
if (fileId) {
|
|
111
|
+
// Open the file tab and switch to files view
|
|
112
|
+
openTab({ fileId, isTransient: false });
|
|
113
|
+
setIsAIChatOpen(false);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const container = diffContainerRef.current;
|
|
121
|
+
const fileNameElements = container.querySelectorAll(
|
|
122
|
+
'.d2h-file-name',
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
// Add click event listeners and cursor pointer style
|
|
126
|
+
fileNameElements.forEach((element) => {
|
|
127
|
+
element.addEventListener(
|
|
128
|
+
'click',
|
|
129
|
+
handleFileNameClick,
|
|
130
|
+
);
|
|
131
|
+
(element as HTMLElement).style.cursor = 'pointer';
|
|
132
|
+
(element as HTMLElement).style.textDecoration =
|
|
133
|
+
'underline';
|
|
134
|
+
(element as HTMLElement).style.color = '#58a6ff'; // GitHub blue link color
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
return () => {
|
|
138
|
+
// Cleanup event listeners
|
|
139
|
+
fileNameElements.forEach((element) => {
|
|
140
|
+
element.removeEventListener(
|
|
141
|
+
'click',
|
|
142
|
+
handleFileNameClick,
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
};
|
|
146
|
+
}, [diffHtml, content, openTab, setIsAIChatOpen]);
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<div className="diff-view">
|
|
150
|
+
<div className="diff-summary">
|
|
151
|
+
<div className="diff-stats">
|
|
152
|
+
<span className="files-changed">
|
|
153
|
+
{unifiedDiffs.length} file
|
|
154
|
+
{unifiedDiffs.length !== 1 ? 's' : ''} changed
|
|
155
|
+
</span>
|
|
156
|
+
{totalAdditions > 0 && (
|
|
157
|
+
<span className="additions">
|
|
158
|
+
+{totalAdditions}
|
|
159
|
+
</span>
|
|
160
|
+
)}
|
|
161
|
+
{totalDeletions > 0 && (
|
|
162
|
+
<span className="deletions">
|
|
163
|
+
-{totalDeletions}
|
|
164
|
+
</span>
|
|
165
|
+
)}
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
|
|
169
|
+
<div
|
|
170
|
+
className="diff-files"
|
|
171
|
+
ref={diffContainerRef}
|
|
172
|
+
dangerouslySetInnerHTML={{ __html: diffHtml }}
|
|
173
|
+
/>
|
|
174
|
+
</div>
|
|
175
|
+
);
|
|
176
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { timestampToDate } from '@vizhub/viz-utils';
|
|
2
|
+
import Markdown from 'react-markdown';
|
|
3
|
+
import remarkGfm from 'remark-gfm';
|
|
4
|
+
import React, {
|
|
5
|
+
useMemo,
|
|
6
|
+
memo,
|
|
7
|
+
useState,
|
|
8
|
+
useContext,
|
|
9
|
+
} from 'react';
|
|
10
|
+
import { DiffView } from './DiffView';
|
|
11
|
+
import { UnifiedFilesDiff } from '../../../utils/fileDiff';
|
|
12
|
+
import { enableDiffView } from '../../featureFlags';
|
|
13
|
+
import { VZCodeContext } from '../../VZCodeContext';
|
|
14
|
+
|
|
15
|
+
interface MessageProps {
|
|
16
|
+
id: string;
|
|
17
|
+
role: 'user' | 'assistant';
|
|
18
|
+
content: string;
|
|
19
|
+
timestamp: number;
|
|
20
|
+
isStreaming?: boolean;
|
|
21
|
+
diffData?: UnifiedFilesDiff;
|
|
22
|
+
chatId?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const MessageComponent = ({
|
|
26
|
+
id,
|
|
27
|
+
role,
|
|
28
|
+
content,
|
|
29
|
+
timestamp,
|
|
30
|
+
isStreaming,
|
|
31
|
+
diffData,
|
|
32
|
+
chatId,
|
|
33
|
+
}: MessageProps) => {
|
|
34
|
+
const { additionalWidgets, handleSendMessage } =
|
|
35
|
+
useContext(VZCodeContext);
|
|
36
|
+
|
|
37
|
+
// Memoize date formatting to avoid repeated computation
|
|
38
|
+
const formattedTime = useMemo(() => {
|
|
39
|
+
return timestampToDate(timestamp).toLocaleTimeString(
|
|
40
|
+
[],
|
|
41
|
+
{
|
|
42
|
+
hour: '2-digit',
|
|
43
|
+
minute: '2-digit',
|
|
44
|
+
},
|
|
45
|
+
);
|
|
46
|
+
}, [timestamp]);
|
|
47
|
+
|
|
48
|
+
// Memoize the className string to avoid recreation
|
|
49
|
+
const messageClassName = useMemo(() => {
|
|
50
|
+
return `ai-chat-message ${role}${isStreaming ? ' streaming' : ''}`;
|
|
51
|
+
}, [role, isStreaming]);
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div className={messageClassName}>
|
|
55
|
+
<div className="ai-chat-message-content">
|
|
56
|
+
<Markdown remarkPlugins={[remarkGfm]}>
|
|
57
|
+
{content}
|
|
58
|
+
</Markdown>
|
|
59
|
+
{enableDiffView &&
|
|
60
|
+
diffData &&
|
|
61
|
+
Object.keys(diffData).length > 0 && (
|
|
62
|
+
<DiffView diffData={diffData} />
|
|
63
|
+
)}
|
|
64
|
+
{additionalWidgets &&
|
|
65
|
+
chatId &&
|
|
66
|
+
React.createElement(additionalWidgets, {
|
|
67
|
+
messageId: id,
|
|
68
|
+
chatId: chatId,
|
|
69
|
+
handleSendMessage,
|
|
70
|
+
})}
|
|
71
|
+
</div>
|
|
72
|
+
<div className="ai-chat-message-time">
|
|
73
|
+
{formattedTime}
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const Message = memo(MessageComponent);
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useRef,
|
|
3
|
+
useEffect,
|
|
4
|
+
useCallback,
|
|
5
|
+
memo,
|
|
6
|
+
useState,
|
|
7
|
+
} from 'react';
|
|
8
|
+
import { Message } from './Message';
|
|
9
|
+
import { TypingIndicator } from './TypingIndicator';
|
|
10
|
+
import { ThinkingScratchpad } from './ThinkingScratchpad';
|
|
11
|
+
import { VizChatMessage } from '@vizhub/viz-types';
|
|
12
|
+
|
|
13
|
+
const MessageListComponent = ({
|
|
14
|
+
messages,
|
|
15
|
+
isLoading,
|
|
16
|
+
chatId, // Add chatId prop
|
|
17
|
+
aiScratchpad, // Add aiScratchpad prop
|
|
18
|
+
aiStatus, // Add aiStatus prop
|
|
19
|
+
}: {
|
|
20
|
+
messages: VizChatMessage[];
|
|
21
|
+
isLoading: boolean;
|
|
22
|
+
chatId?: string; // Add chatId to the type
|
|
23
|
+
aiScratchpad?: string; // Add aiScratchpad to the type
|
|
24
|
+
aiStatus?: string; // Add aiStatus to the type
|
|
25
|
+
}) => {
|
|
26
|
+
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
27
|
+
const messagesContainerRef = useRef<HTMLDivElement>(null);
|
|
28
|
+
const [isUserScrolled, setIsUserScrolled] =
|
|
29
|
+
useState(false);
|
|
30
|
+
const [autoScrollEnabled, setAutoScrollEnabled] =
|
|
31
|
+
useState(true);
|
|
32
|
+
const scrollTimeoutRef =
|
|
33
|
+
useRef<ReturnType<typeof setTimeout>>();
|
|
34
|
+
|
|
35
|
+
// Check if the user is scrolled to the bottom
|
|
36
|
+
const isScrolledToBottom = useCallback(() => {
|
|
37
|
+
const container = messagesContainerRef.current;
|
|
38
|
+
if (!container) return true;
|
|
39
|
+
|
|
40
|
+
const threshold = 50; // Allow 50px tolerance for "at bottom"
|
|
41
|
+
const { scrollTop, scrollHeight, clientHeight } =
|
|
42
|
+
container;
|
|
43
|
+
return (
|
|
44
|
+
scrollHeight - scrollTop - clientHeight < threshold
|
|
45
|
+
);
|
|
46
|
+
}, []);
|
|
47
|
+
|
|
48
|
+
// Smooth scroll to bottom with linear transition
|
|
49
|
+
const scrollToBottom = useCallback(() => {
|
|
50
|
+
if (!autoScrollEnabled) return;
|
|
51
|
+
|
|
52
|
+
messagesEndRef.current?.scrollIntoView({
|
|
53
|
+
behavior: 'smooth',
|
|
54
|
+
block: 'end',
|
|
55
|
+
});
|
|
56
|
+
}, [autoScrollEnabled]);
|
|
57
|
+
|
|
58
|
+
// Handle scroll events to detect user manual scrolling
|
|
59
|
+
const handleScroll = useCallback(() => {
|
|
60
|
+
const container = messagesContainerRef.current;
|
|
61
|
+
if (!container) return;
|
|
62
|
+
|
|
63
|
+
const isAtBottom = isScrolledToBottom();
|
|
64
|
+
|
|
65
|
+
// Clear any existing timeout
|
|
66
|
+
if (scrollTimeoutRef.current) {
|
|
67
|
+
clearTimeout(scrollTimeoutRef.current);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// If user scrolled up from bottom, disable auto-scroll
|
|
71
|
+
if (!isAtBottom && !isUserScrolled) {
|
|
72
|
+
setIsUserScrolled(true);
|
|
73
|
+
setAutoScrollEnabled(false);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// If user scrolled back to bottom, re-enable auto-scroll after a brief delay
|
|
77
|
+
if (isAtBottom && isUserScrolled) {
|
|
78
|
+
scrollTimeoutRef.current = setTimeout(() => {
|
|
79
|
+
setIsUserScrolled(false);
|
|
80
|
+
setAutoScrollEnabled(true);
|
|
81
|
+
}, 500); // 500ms delay to prevent flickering
|
|
82
|
+
}
|
|
83
|
+
}, [isUserScrolled, isScrolledToBottom]);
|
|
84
|
+
|
|
85
|
+
// Auto-scroll when messages change, but only if auto-scroll is enabled
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (autoScrollEnabled && !isUserScrolled) {
|
|
88
|
+
// Use a short debounce to prevent multiple scroll calls during rapid updates
|
|
89
|
+
if (scrollTimeoutRef.current) {
|
|
90
|
+
clearTimeout(scrollTimeoutRef.current);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
scrollTimeoutRef.current = setTimeout(() => {
|
|
94
|
+
scrollToBottom();
|
|
95
|
+
}, 100); // 100ms debounce
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return () => {
|
|
99
|
+
if (scrollTimeoutRef.current) {
|
|
100
|
+
clearTimeout(scrollTimeoutRef.current);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}, [
|
|
104
|
+
messages,
|
|
105
|
+
autoScrollEnabled,
|
|
106
|
+
isUserScrolled,
|
|
107
|
+
scrollToBottom,
|
|
108
|
+
]);
|
|
109
|
+
|
|
110
|
+
// Track previous loading state to detect when AI generation completes
|
|
111
|
+
const [prevIsLoading, setPrevIsLoading] =
|
|
112
|
+
useState(isLoading);
|
|
113
|
+
|
|
114
|
+
// Scroll to bottom when AI generation completes (loading changes from true to false)
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
// If AI was generating and now it's finished, scroll to bottom
|
|
117
|
+
if (prevIsLoading && !isLoading) {
|
|
118
|
+
// Force scroll to bottom regardless of user scroll state
|
|
119
|
+
// This ensures we always scroll to bottom when generation completes
|
|
120
|
+
messagesEndRef.current?.scrollIntoView({
|
|
121
|
+
behavior: 'smooth',
|
|
122
|
+
block: 'end',
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Re-enable auto-scroll for future messages
|
|
126
|
+
setIsUserScrolled(false);
|
|
127
|
+
setAutoScrollEnabled(true);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
setPrevIsLoading(isLoading);
|
|
131
|
+
}, [isLoading, prevIsLoading]);
|
|
132
|
+
|
|
133
|
+
// Clean up timeout on unmount
|
|
134
|
+
useEffect(() => {
|
|
135
|
+
return () => {
|
|
136
|
+
if (scrollTimeoutRef.current) {
|
|
137
|
+
clearTimeout(scrollTimeoutRef.current);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
}, []);
|
|
141
|
+
|
|
142
|
+
// Check if AI generation has started (last message is from assistant)
|
|
143
|
+
const lastMessage = messages[messages.length - 1];
|
|
144
|
+
const aiGenerationStarted =
|
|
145
|
+
lastMessage?.role === 'assistant' &&
|
|
146
|
+
lastMessage.content !== '';
|
|
147
|
+
|
|
148
|
+
// Show typing indicator only when loading and AI generation hasn't started yet
|
|
149
|
+
const showTypingIndicator =
|
|
150
|
+
isLoading && !aiGenerationStarted;
|
|
151
|
+
|
|
152
|
+
// Show thinking scratchpad when AI is thinking (has scratchpad content)
|
|
153
|
+
const showThinkingScratchpad = Boolean(
|
|
154
|
+
aiScratchpad && aiScratchpad.trim(),
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<div
|
|
159
|
+
className="ai-chat-messages"
|
|
160
|
+
ref={messagesContainerRef}
|
|
161
|
+
onScroll={handleScroll}
|
|
162
|
+
>
|
|
163
|
+
{messages.map((msg, index) => {
|
|
164
|
+
return (
|
|
165
|
+
<Message
|
|
166
|
+
key={msg.id}
|
|
167
|
+
id={msg.id}
|
|
168
|
+
role={msg.role}
|
|
169
|
+
content={msg.content}
|
|
170
|
+
timestamp={msg.timestamp}
|
|
171
|
+
diffData={(msg as any).diffData}
|
|
172
|
+
chatId={chatId}
|
|
173
|
+
/>
|
|
174
|
+
);
|
|
175
|
+
})}
|
|
176
|
+
|
|
177
|
+
{showThinkingScratchpad && (
|
|
178
|
+
<ThinkingScratchpad
|
|
179
|
+
content={aiScratchpad || ''}
|
|
180
|
+
isVisible={showThinkingScratchpad}
|
|
181
|
+
/>
|
|
182
|
+
)}
|
|
183
|
+
|
|
184
|
+
{showTypingIndicator && <TypingIndicator />}
|
|
185
|
+
<div ref={messagesEndRef} />
|
|
186
|
+
</div>
|
|
187
|
+
);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
export const MessageList = memo(MessageListComponent);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import Markdown from 'react-markdown';
|
|
2
|
+
import remarkGfm from 'remark-gfm';
|
|
3
|
+
import { memo } from 'react';
|
|
4
|
+
|
|
5
|
+
interface StreamingMessageProps {
|
|
6
|
+
content: string;
|
|
7
|
+
status?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const StreamingMessageComponent = ({
|
|
11
|
+
content,
|
|
12
|
+
status,
|
|
13
|
+
}: StreamingMessageProps) => {
|
|
14
|
+
// Don't render if there's no content and no status
|
|
15
|
+
if (!content.trim()) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div className="ai-chat-message assistant streaming">
|
|
21
|
+
{status && (
|
|
22
|
+
<div className="ai-chat-status">{status}</div>
|
|
23
|
+
)}
|
|
24
|
+
<div className="ai-chat-message-content">
|
|
25
|
+
<Markdown remarkPlugins={[remarkGfm]}>
|
|
26
|
+
{content}
|
|
27
|
+
</Markdown>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const StreamingMessage = memo(
|
|
34
|
+
StreamingMessageComponent,
|
|
35
|
+
);
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import {
|
|
2
|
+
memo,
|
|
3
|
+
useRef,
|
|
4
|
+
useEffect,
|
|
5
|
+
useCallback,
|
|
6
|
+
useState,
|
|
7
|
+
} from 'react';
|
|
8
|
+
import Markdown from 'react-markdown';
|
|
9
|
+
import remarkGfm from 'remark-gfm';
|
|
10
|
+
|
|
11
|
+
interface ThinkingScratchpadProps {
|
|
12
|
+
content: string;
|
|
13
|
+
isVisible: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const ThinkingScratchpadComponent = ({
|
|
17
|
+
content,
|
|
18
|
+
isVisible,
|
|
19
|
+
}: ThinkingScratchpadProps) => {
|
|
20
|
+
const contentRef = useRef<HTMLDivElement>(null);
|
|
21
|
+
const [isUserScrolled, setIsUserScrolled] =
|
|
22
|
+
useState(false);
|
|
23
|
+
const [autoScrollEnabled, setAutoScrollEnabled] =
|
|
24
|
+
useState(true);
|
|
25
|
+
const scrollTimeoutRef =
|
|
26
|
+
useRef<ReturnType<typeof setTimeout>>();
|
|
27
|
+
|
|
28
|
+
// Check if the user is scrolled to the bottom
|
|
29
|
+
const isScrolledToBottom = useCallback(() => {
|
|
30
|
+
const container = contentRef.current;
|
|
31
|
+
if (!container) return true;
|
|
32
|
+
|
|
33
|
+
const threshold = 10; // Allow 10px tolerance for "at bottom"
|
|
34
|
+
const { scrollTop, scrollHeight, clientHeight } =
|
|
35
|
+
container;
|
|
36
|
+
return (
|
|
37
|
+
scrollHeight - scrollTop - clientHeight < threshold
|
|
38
|
+
);
|
|
39
|
+
}, []);
|
|
40
|
+
|
|
41
|
+
// Smooth scroll to bottom
|
|
42
|
+
const scrollToBottom = useCallback(() => {
|
|
43
|
+
if (!autoScrollEnabled) return;
|
|
44
|
+
|
|
45
|
+
const container = contentRef.current;
|
|
46
|
+
if (container) {
|
|
47
|
+
container.scrollTo({
|
|
48
|
+
top: container.scrollHeight,
|
|
49
|
+
behavior: 'smooth',
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}, [autoScrollEnabled]);
|
|
53
|
+
|
|
54
|
+
// Handle scroll events to detect user manual scrolling
|
|
55
|
+
const handleScroll = useCallback(() => {
|
|
56
|
+
const container = contentRef.current;
|
|
57
|
+
if (!container) return;
|
|
58
|
+
|
|
59
|
+
const isAtBottom = isScrolledToBottom();
|
|
60
|
+
|
|
61
|
+
// Clear any existing timeout
|
|
62
|
+
if (scrollTimeoutRef.current) {
|
|
63
|
+
clearTimeout(scrollTimeoutRef.current);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// If user scrolled up from bottom, disable auto-scroll
|
|
67
|
+
if (!isAtBottom && !isUserScrolled) {
|
|
68
|
+
setIsUserScrolled(true);
|
|
69
|
+
setAutoScrollEnabled(false);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// If user scrolled back to bottom, re-enable auto-scroll after a brief delay
|
|
73
|
+
if (isAtBottom && isUserScrolled) {
|
|
74
|
+
scrollTimeoutRef.current = setTimeout(() => {
|
|
75
|
+
setIsUserScrolled(false);
|
|
76
|
+
setAutoScrollEnabled(true);
|
|
77
|
+
}, 300); // 300ms delay to prevent flickering
|
|
78
|
+
}
|
|
79
|
+
}, [isUserScrolled, isScrolledToBottom]);
|
|
80
|
+
|
|
81
|
+
// Auto-scroll when content changes, but only if auto-scroll is enabled
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (
|
|
84
|
+
autoScrollEnabled &&
|
|
85
|
+
!isUserScrolled &&
|
|
86
|
+
isVisible &&
|
|
87
|
+
content
|
|
88
|
+
) {
|
|
89
|
+
// Use a short debounce to prevent multiple scroll calls during rapid updates
|
|
90
|
+
if (scrollTimeoutRef.current) {
|
|
91
|
+
clearTimeout(scrollTimeoutRef.current);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
scrollTimeoutRef.current = setTimeout(() => {
|
|
95
|
+
scrollToBottom();
|
|
96
|
+
}, 50); // 50ms debounce
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return () => {
|
|
100
|
+
if (scrollTimeoutRef.current) {
|
|
101
|
+
clearTimeout(scrollTimeoutRef.current);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}, [
|
|
105
|
+
content,
|
|
106
|
+
autoScrollEnabled,
|
|
107
|
+
isUserScrolled,
|
|
108
|
+
isVisible,
|
|
109
|
+
scrollToBottom,
|
|
110
|
+
]);
|
|
111
|
+
|
|
112
|
+
// Clean up timeout on unmount
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
return () => {
|
|
115
|
+
if (scrollTimeoutRef.current) {
|
|
116
|
+
clearTimeout(scrollTimeoutRef.current);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
}, []);
|
|
120
|
+
|
|
121
|
+
// Reset scroll state when component becomes visible
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
if (isVisible) {
|
|
124
|
+
setIsUserScrolled(false);
|
|
125
|
+
setAutoScrollEnabled(true);
|
|
126
|
+
}
|
|
127
|
+
}, [isVisible]);
|
|
128
|
+
|
|
129
|
+
if (!isVisible || !content) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<div className="thinking-scratchpad">
|
|
135
|
+
<div className="thinking-scratchpad-header">
|
|
136
|
+
<span className="thinking-scratchpad-icon">🧠</span>
|
|
137
|
+
<span className="thinking-scratchpad-title">
|
|
138
|
+
AI is thinking...
|
|
139
|
+
</span>
|
|
140
|
+
</div>
|
|
141
|
+
<div
|
|
142
|
+
className="thinking-scratchpad-content"
|
|
143
|
+
ref={contentRef}
|
|
144
|
+
onScroll={handleScroll}
|
|
145
|
+
>
|
|
146
|
+
<Markdown remarkPlugins={[remarkGfm]}>
|
|
147
|
+
{content}
|
|
148
|
+
</Markdown>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
export const ThinkingScratchpad = memo(
|
|
155
|
+
ThinkingScratchpadComponent,
|
|
156
|
+
);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { memo } from 'react';
|
|
2
|
+
|
|
3
|
+
const TypingIndicatorComponent = () => {
|
|
4
|
+
return (
|
|
5
|
+
<div className="ai-chat-message assistant">
|
|
6
|
+
<div className="ai-chat-message-content">
|
|
7
|
+
<div className="ai-chat-typing">
|
|
8
|
+
<span></span>
|
|
9
|
+
<span></span>
|
|
10
|
+
<span></span>
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const TypingIndicator = memo(
|
|
18
|
+
TypingIndicatorComponent,
|
|
19
|
+
);
|