vzcode 1.44.0 → 1.46.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-CRLuRnCr.js → buildWorker-C_tGhWXG.js} +135 -74
- package/dist/assets/index-CC0CUUDi.js +457 -0
- package/dist/assets/{index-Beqaj63x.css → index-CQyHWTWE.css} +1 -1
- package/dist/assets/worker-CekYspLa.js +453 -0
- package/dist/index.html +2 -2
- package/package.json +13 -7
- package/src/client/CodeEditor/{getOrCreateEditor.ts → getOrCreateEditor.tsx} +129 -1
- package/src/client/CodeEditor/index.tsx +11 -4
- package/src/client/Icons/SparklesSVG.tsx +9 -3
- package/src/client/RunCodeWidget/index.tsx +18 -15
- package/src/client/SplitPaneResizeContext.tsx +26 -108
- package/src/client/VZCodeContext.tsx +15 -1
- package/src/client/VZRight.tsx +32 -4
- package/src/client/VZSidebar/AIChat/ChatInput.tsx +80 -10
- package/src/client/VZSidebar/AIChat/DiffView.scss +154 -0
- package/src/client/VZSidebar/AIChat/DiffView.tsx +133 -0
- package/src/client/VZSidebar/AIChat/Message.tsx +24 -0
- package/src/client/VZSidebar/AIChat/MessageList.tsx +134 -12
- package/src/client/VZSidebar/AIChat/ThinkingScratchpad.tsx +37 -0
- package/src/client/VZSidebar/AIChat/index.tsx +155 -17
- package/src/client/VZSidebar/AIChat/styles.scss +223 -1
- package/src/client/VZSidebar/aiCopyPaste.ts +15 -2
- package/src/client/VZSidebar/index.tsx +10 -2
- package/src/client/bootstrap.ts +11 -1
- package/src/client/featureFlags.ts +2 -0
- package/src/client/useActions.ts +12 -0
- package/src/client/usePrettier/index.ts +1 -3
- package/src/client/vzReducer/aiChatReducer.ts +13 -0
- package/src/client/vzReducer/createInitialState.ts +1 -0
- package/src/client/vzReducer/index.ts +9 -0
- package/src/client/vzReducer/searchReducer.test.ts +44 -2
- package/src/runCode.ts +5 -17
- package/src/server/aiChatHandler/aiEditing.ts +56 -0
- package/src/server/aiChatHandler/chatOperations.ts +78 -1
- package/src/server/aiChatHandler/index.ts +33 -9
- package/src/server/aiChatHandler/llmStreaming.ts +101 -19
- package/src/server/computeInitialDocument.ts +25 -6
- package/src/server/generateAIResponse.ts +4 -0
- package/src/server/index.ts +83 -2
- package/src/utils/fileDiff.ts +114 -0
- package/dist/assets/index-C46Hp5ym.js +0 -340
- package/dist/assets/worker-DkYJN5WQ.js +0 -453
package/src/client/VZRight.tsx
CHANGED
|
@@ -11,6 +11,14 @@ import {
|
|
|
11
11
|
import BuildWorker from './buildWorker?worker';
|
|
12
12
|
import { VZCodeContext } from './VZCodeContext';
|
|
13
13
|
import { vizFilesToFileCollection } from '@vizhub/viz-utils';
|
|
14
|
+
import { VizContent } from '@vizhub/viz-types';
|
|
15
|
+
|
|
16
|
+
// Extend VizContent type to include hardRerun property
|
|
17
|
+
type ExtendedVizContent = VizContent & {
|
|
18
|
+
// TODO remove this, use the runId property from VizContent instead.
|
|
19
|
+
// If `VizContent.runId` changes, it will trigger a hard rerun.
|
|
20
|
+
hardRerun?: boolean;
|
|
21
|
+
};
|
|
14
22
|
|
|
15
23
|
const enableIframe = true;
|
|
16
24
|
|
|
@@ -18,6 +26,9 @@ export const VZRight = () => {
|
|
|
18
26
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
19
27
|
const runtimeRef = useRef<VizHubRuntime | null>(null);
|
|
20
28
|
const isFirstRunRef = useRef(true);
|
|
29
|
+
const lastRunIdRef = useRef<string | undefined>(
|
|
30
|
+
undefined,
|
|
31
|
+
);
|
|
21
32
|
|
|
22
33
|
// Get access to the current files.
|
|
23
34
|
const { content, handleRuntimeError, clearRuntimeError } =
|
|
@@ -32,6 +43,15 @@ export const VZRight = () => {
|
|
|
32
43
|
);
|
|
33
44
|
|
|
34
45
|
const isInteracting = content?.isInteracting || false;
|
|
46
|
+
const runId = content?.runId;
|
|
47
|
+
const hardRerun =
|
|
48
|
+
(content as ExtendedVizContent)?.hardRerun || false;
|
|
49
|
+
|
|
50
|
+
// Check if runId has changed
|
|
51
|
+
const runIdChanged = runId !== lastRunIdRef.current;
|
|
52
|
+
if (runIdChanged) {
|
|
53
|
+
lastRunIdRef.current = runId;
|
|
54
|
+
}
|
|
35
55
|
|
|
36
56
|
useEffect(() => {
|
|
37
57
|
if (!files) return;
|
|
@@ -51,14 +71,22 @@ export const VZRight = () => {
|
|
|
51
71
|
});
|
|
52
72
|
}
|
|
53
73
|
|
|
54
|
-
// Run code in the iframe
|
|
55
|
-
|
|
74
|
+
// Run code in the iframe when:
|
|
75
|
+
// 1. First run
|
|
76
|
+
// 2. User is interacting with widgets
|
|
77
|
+
// 3. runId changed (indicating AI finished or other trigger)
|
|
78
|
+
if (
|
|
79
|
+
isFirstRunRef.current ||
|
|
80
|
+
isInteracting ||
|
|
81
|
+
runIdChanged
|
|
82
|
+
) {
|
|
56
83
|
// Clear runtime errors when new code runs
|
|
57
84
|
clearRuntimeError();
|
|
58
85
|
|
|
59
86
|
runtimeRef.current?.run({
|
|
60
87
|
files,
|
|
61
|
-
|
|
88
|
+
// Enable hot reloading when interacting, disable when runId changed without interaction
|
|
89
|
+
enableHotReloading: isInteracting,
|
|
62
90
|
enableSourcemap: true,
|
|
63
91
|
vizId: 'example-viz',
|
|
64
92
|
});
|
|
@@ -70,7 +98,7 @@ export const VZRight = () => {
|
|
|
70
98
|
// runtime.cleanup();
|
|
71
99
|
// worker.terminate();
|
|
72
100
|
// };
|
|
73
|
-
}, [files, isInteracting]);
|
|
101
|
+
}, [files, isInteracting, runIdChanged, hardRerun]);
|
|
74
102
|
|
|
75
103
|
return (
|
|
76
104
|
<div className="right">
|
|
@@ -4,22 +4,31 @@ import {
|
|
|
4
4
|
useCallback,
|
|
5
5
|
memo,
|
|
6
6
|
} from 'react';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
Form,
|
|
9
|
+
Button,
|
|
10
|
+
ButtonGroup,
|
|
11
|
+
ToggleButton,
|
|
12
|
+
} from '../../bootstrap';
|
|
8
13
|
|
|
9
14
|
interface ChatInputProps {
|
|
10
|
-
|
|
11
|
-
|
|
15
|
+
aiChatMessage: string;
|
|
16
|
+
setAIChatMessage: (message: string) => void;
|
|
12
17
|
onSendMessage: () => void;
|
|
13
18
|
isLoading: boolean;
|
|
14
19
|
focused: boolean;
|
|
20
|
+
aiChatMode: 'ask' | 'edit';
|
|
21
|
+
setAIChatMode: (mode: 'ask' | 'edit') => void;
|
|
15
22
|
}
|
|
16
23
|
|
|
17
24
|
const ChatInputComponent = ({
|
|
18
|
-
|
|
19
|
-
|
|
25
|
+
aiChatMessage,
|
|
26
|
+
setAIChatMessage,
|
|
20
27
|
onSendMessage,
|
|
21
28
|
isLoading,
|
|
22
29
|
focused,
|
|
30
|
+
aiChatMode,
|
|
31
|
+
setAIChatMode,
|
|
23
32
|
}: ChatInputProps) => {
|
|
24
33
|
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
25
34
|
|
|
@@ -42,30 +51,91 @@ const ChatInputComponent = ({
|
|
|
42
51
|
|
|
43
52
|
const handleChange = useCallback(
|
|
44
53
|
(event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
45
|
-
|
|
54
|
+
setAIChatMessage(event.target.value);
|
|
46
55
|
},
|
|
47
|
-
[
|
|
56
|
+
[setAIChatMessage],
|
|
48
57
|
);
|
|
49
58
|
|
|
50
59
|
return (
|
|
51
60
|
<div className="ai-chat-input-container">
|
|
61
|
+
<div
|
|
62
|
+
className="ai-chat-mode-toggle"
|
|
63
|
+
style={{ marginBottom: '8px' }}
|
|
64
|
+
>
|
|
65
|
+
<ButtonGroup size="sm">
|
|
66
|
+
<ToggleButton
|
|
67
|
+
id="ai-chat-mode-ask"
|
|
68
|
+
type="radio"
|
|
69
|
+
variant={
|
|
70
|
+
aiChatMode === 'ask'
|
|
71
|
+
? 'primary'
|
|
72
|
+
: 'outline-primary'
|
|
73
|
+
}
|
|
74
|
+
name="ai-chat-mode"
|
|
75
|
+
value="ask"
|
|
76
|
+
checked={aiChatMode === 'ask'}
|
|
77
|
+
onChange={() => setAIChatMode('ask')}
|
|
78
|
+
disabled={isLoading}
|
|
79
|
+
>
|
|
80
|
+
💬 Ask
|
|
81
|
+
</ToggleButton>
|
|
82
|
+
<ToggleButton
|
|
83
|
+
id="ai-chat-mode-edit"
|
|
84
|
+
type="radio"
|
|
85
|
+
variant={
|
|
86
|
+
aiChatMode === 'edit'
|
|
87
|
+
? 'primary'
|
|
88
|
+
: 'outline-primary'
|
|
89
|
+
}
|
|
90
|
+
name="ai-chat-mode"
|
|
91
|
+
value="edit"
|
|
92
|
+
checked={aiChatMode === 'edit'}
|
|
93
|
+
onChange={() => setAIChatMode('edit')}
|
|
94
|
+
disabled={isLoading}
|
|
95
|
+
>
|
|
96
|
+
✏️ Edit
|
|
97
|
+
</ToggleButton>
|
|
98
|
+
</ButtonGroup>
|
|
99
|
+
<div className="ai-chat-mode-description">
|
|
100
|
+
{aiChatMode === 'ask'
|
|
101
|
+
? 'Ask questions without editing files'
|
|
102
|
+
: 'Get answers and code edits'}
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
52
105
|
<Form.Group className="ai-chat-input-group">
|
|
53
106
|
<Form.Control
|
|
54
107
|
as="textarea"
|
|
55
108
|
rows={2}
|
|
56
|
-
value={
|
|
109
|
+
value={aiChatMessage}
|
|
57
110
|
onChange={handleChange}
|
|
58
111
|
onKeyDown={handleKeyDown}
|
|
59
112
|
ref={inputRef}
|
|
60
|
-
placeholder=
|
|
113
|
+
placeholder={
|
|
114
|
+
aiChatMode === 'edit'
|
|
115
|
+
? 'Ask me to make code changes...'
|
|
116
|
+
: 'Ask me anything about your code...'
|
|
117
|
+
}
|
|
61
118
|
spellCheck="false"
|
|
62
119
|
disabled={isLoading}
|
|
120
|
+
aria-label="Chat message input"
|
|
63
121
|
/>
|
|
122
|
+
{aiChatMessage && (
|
|
123
|
+
<div className="ai-chat-input-info">
|
|
124
|
+
<span className="ai-chat-hint">
|
|
125
|
+
Shift+Enter for new line
|
|
126
|
+
</span>
|
|
127
|
+
<span className="ai-chat-hint">
|
|
128
|
+
Press Enter to send
|
|
129
|
+
</span>
|
|
130
|
+
</div>
|
|
131
|
+
)}
|
|
64
132
|
<Button
|
|
65
133
|
variant="primary"
|
|
66
134
|
onClick={onSendMessage}
|
|
67
|
-
disabled={!
|
|
135
|
+
disabled={!aiChatMessage.trim() || isLoading}
|
|
68
136
|
className="ai-chat-send-button"
|
|
137
|
+
aria-label="Send message"
|
|
138
|
+
title="Send message (Enter)"
|
|
69
139
|
>
|
|
70
140
|
Send
|
|
71
141
|
</Button>
|
|
@@ -0,0 +1,154 @@
|
|
|
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: 12px;
|
|
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
|
+
.undo-button-container {
|
|
35
|
+
display: flex;
|
|
36
|
+
justify-content: flex-end;
|
|
37
|
+
margin-top: 16px;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.undo-button {
|
|
41
|
+
background-color: #dc3545;
|
|
42
|
+
color: white;
|
|
43
|
+
border: none;
|
|
44
|
+
border-radius: 4px;
|
|
45
|
+
padding: 4px 8px;
|
|
46
|
+
font-size: 12px;
|
|
47
|
+
cursor: pointer;
|
|
48
|
+
transition: background-color 0.2s;
|
|
49
|
+
|
|
50
|
+
&:hover:not(:disabled) {
|
|
51
|
+
background-color: #c82333;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
&:disabled {
|
|
55
|
+
background-color: #6c757d;
|
|
56
|
+
cursor: not-allowed;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.diff-files {
|
|
61
|
+
display: flex;
|
|
62
|
+
flex-direction: column;
|
|
63
|
+
gap: 4px;
|
|
64
|
+
|
|
65
|
+
// Fix diff2html styling issues for dark theme and layout
|
|
66
|
+
.d2h-wrapper {
|
|
67
|
+
// Override diff2html variables for dark theme
|
|
68
|
+
--d2h-bg-color: rgb(41, 44, 52);
|
|
69
|
+
--d2h-color: #e6edf3;
|
|
70
|
+
--d2h-border-color: #30363d;
|
|
71
|
+
--d2h-dim-color: #6e7681;
|
|
72
|
+
--d2h-info-bg-color: #161b22;
|
|
73
|
+
--d2h-file-header-bg-color: #161b22;
|
|
74
|
+
--d2h-file-header-border-color: #30363d;
|
|
75
|
+
--d2h-line-border-color: #21262d;
|
|
76
|
+
--d2h-ins-bg-color: rgba(46, 160, 67, 0.15);
|
|
77
|
+
--d2h-ins-border-color: rgba(46, 160, 67, 0.4);
|
|
78
|
+
--d2h-ins-highlight-bg-color: rgba(46, 160, 67, 0.4);
|
|
79
|
+
--d2h-ins-label-color: #3fb950;
|
|
80
|
+
--d2h-del-bg-color: rgba(248, 81, 73, 0.1);
|
|
81
|
+
--d2h-del-border-color: rgba(248, 81, 73, 0.4);
|
|
82
|
+
--d2h-del-highlight-bg-color: rgba(248, 81, 73, 0.4);
|
|
83
|
+
--d2h-del-label-color: #f85149;
|
|
84
|
+
|
|
85
|
+
background-color: var(--d2h-bg-color);
|
|
86
|
+
color: var(--d2h-color);
|
|
87
|
+
border-radius: 6px;
|
|
88
|
+
overflow: hidden;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Fix layout issues with line numbers
|
|
92
|
+
.d2h-diff-table {
|
|
93
|
+
position: relative;
|
|
94
|
+
background-color: var(--d2h-bg-color);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.d2h-code-linenumber {
|
|
98
|
+
position: relative !important;
|
|
99
|
+
float: none !important;
|
|
100
|
+
display: table-cell !important;
|
|
101
|
+
width: auto !important;
|
|
102
|
+
background-color: var(--d2h-bg-color) !important;
|
|
103
|
+
border-color: var(--d2h-line-border-color) !important;
|
|
104
|
+
color: var(--d2h-dim-color) !important;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.d2h-code-side-linenumber {
|
|
108
|
+
position: relative !important;
|
|
109
|
+
float: none !important;
|
|
110
|
+
display: table-cell !important;
|
|
111
|
+
width: auto !important;
|
|
112
|
+
background-color: var(--d2h-bg-color) !important;
|
|
113
|
+
border-color: var(--d2h-line-border-color) !important;
|
|
114
|
+
color: var(--d2h-dim-color) !important;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Ensure proper table layout
|
|
118
|
+
.d2h-diff-tbody tr {
|
|
119
|
+
display: table-row;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.d2h-diff-tbody td {
|
|
123
|
+
display: table-cell;
|
|
124
|
+
vertical-align: top;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Fix file header styling
|
|
128
|
+
.d2h-file-header {
|
|
129
|
+
background-color: var(
|
|
130
|
+
--d2h-file-header-bg-color
|
|
131
|
+
) !important;
|
|
132
|
+
border-bottom: 1px solid
|
|
133
|
+
var(--d2h-file-header-border-color) !important;
|
|
134
|
+
color: var(--d2h-color);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Ensure proper line styling
|
|
138
|
+
.d2h-del {
|
|
139
|
+
background-color: var(--d2h-del-bg-color) !important;
|
|
140
|
+
border-color: var(--d2h-del-border-color) !important;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.d2h-ins {
|
|
144
|
+
background-color: var(--d2h-ins-bg-color) !important;
|
|
145
|
+
border-color: var(--d2h-ins-border-color) !important;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Fix wrapper border
|
|
149
|
+
.d2h-file-wrapper {
|
|
150
|
+
border: 1px solid var(--d2h-border-color) !important;
|
|
151
|
+
background-color: var(--d2h-bg-color);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
UnifiedFilesDiff,
|
|
4
|
+
parseUnifiedDiffStats,
|
|
5
|
+
combineUnifiedDiffs,
|
|
6
|
+
} from '../../../utils/fileDiff';
|
|
7
|
+
import * as Diff2Html from 'diff2html';
|
|
8
|
+
import 'diff2html/bundles/css/diff2html.min.css';
|
|
9
|
+
import './DiffView.scss';
|
|
10
|
+
|
|
11
|
+
interface DiffViewProps {
|
|
12
|
+
diffData: UnifiedFilesDiff;
|
|
13
|
+
messageId?: string;
|
|
14
|
+
chatId?: string;
|
|
15
|
+
beforeFiles?: any;
|
|
16
|
+
canUndo?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const DiffView: React.FC<DiffViewProps> = ({
|
|
20
|
+
diffData,
|
|
21
|
+
messageId,
|
|
22
|
+
chatId,
|
|
23
|
+
beforeFiles,
|
|
24
|
+
canUndo = false,
|
|
25
|
+
}) => {
|
|
26
|
+
const [isUndoing, setIsUndoing] = useState(false);
|
|
27
|
+
|
|
28
|
+
const unifiedDiffs = Object.values(diffData).filter(
|
|
29
|
+
(diff) => diff.length > 0,
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
if (unifiedDiffs.length === 0) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const handleUndo = async () => {
|
|
37
|
+
if (
|
|
38
|
+
!messageId ||
|
|
39
|
+
!chatId ||
|
|
40
|
+
!beforeFiles ||
|
|
41
|
+
isUndoing
|
|
42
|
+
) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
setIsUndoing(true);
|
|
47
|
+
try {
|
|
48
|
+
const response = await fetch('/ai-chat-undo', {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
headers: {
|
|
51
|
+
'Content-Type': 'application/json',
|
|
52
|
+
},
|
|
53
|
+
body: JSON.stringify({
|
|
54
|
+
chatId,
|
|
55
|
+
messageId,
|
|
56
|
+
}),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (!response.ok) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`HTTP error! status: ${response.status}`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// The server will handle the ShareDB operations to undo the changes
|
|
66
|
+
// The UI will update automatically via ShareDB
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.error('Error undoing AI edit:', error);
|
|
69
|
+
// TODO: Show user-friendly error message
|
|
70
|
+
} finally {
|
|
71
|
+
setIsUndoing(false);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// Calculate statistics from all unified diffs
|
|
76
|
+
let totalAdditions = 0;
|
|
77
|
+
let totalDeletions = 0;
|
|
78
|
+
|
|
79
|
+
for (const unifiedDiff of unifiedDiffs) {
|
|
80
|
+
const stats = parseUnifiedDiffStats(unifiedDiff);
|
|
81
|
+
totalAdditions += stats.additions;
|
|
82
|
+
totalDeletions += stats.deletions;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Combine all unified diffs and convert to HTML using diff2html
|
|
86
|
+
const combinedUnifiedDiff = combineUnifiedDiffs(diffData);
|
|
87
|
+
const diffHtml = Diff2Html.html(combinedUnifiedDiff, {
|
|
88
|
+
drawFileList: false,
|
|
89
|
+
matching: 'words',
|
|
90
|
+
diffStyle: 'word',
|
|
91
|
+
outputFormat: 'line-by-line',
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<div className="diff-view">
|
|
96
|
+
<div className="diff-summary">
|
|
97
|
+
<div className="diff-stats">
|
|
98
|
+
<span className="files-changed">
|
|
99
|
+
{unifiedDiffs.length} file
|
|
100
|
+
{unifiedDiffs.length !== 1 ? 's' : ''} changed
|
|
101
|
+
</span>
|
|
102
|
+
{totalAdditions > 0 && (
|
|
103
|
+
<span className="additions">
|
|
104
|
+
+{totalAdditions}
|
|
105
|
+
</span>
|
|
106
|
+
)}
|
|
107
|
+
{totalDeletions > 0 && (
|
|
108
|
+
<span className="deletions">
|
|
109
|
+
-{totalDeletions}
|
|
110
|
+
</span>
|
|
111
|
+
)}
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<div
|
|
116
|
+
className="diff-files"
|
|
117
|
+
dangerouslySetInnerHTML={{ __html: diffHtml }}
|
|
118
|
+
/>
|
|
119
|
+
{canUndo && beforeFiles && (
|
|
120
|
+
<div className="undo-button-container">
|
|
121
|
+
<button
|
|
122
|
+
className="undo-button"
|
|
123
|
+
onClick={handleUndo}
|
|
124
|
+
disabled={isUndoing}
|
|
125
|
+
title="Undo this AI edit"
|
|
126
|
+
>
|
|
127
|
+
{isUndoing ? 'Undoing...' : 'Undo'}
|
|
128
|
+
</button>
|
|
129
|
+
</div>
|
|
130
|
+
)}
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
};
|
|
@@ -2,6 +2,9 @@ import { timestampToDate } from '@vizhub/viz-utils';
|
|
|
2
2
|
import Markdown from 'react-markdown';
|
|
3
3
|
import remarkGfm from 'remark-gfm';
|
|
4
4
|
import { useMemo, memo } from 'react';
|
|
5
|
+
import { DiffView } from './DiffView';
|
|
6
|
+
import { UnifiedFilesDiff } from '../../../utils/fileDiff';
|
|
7
|
+
import { enableDiffView } from '../../featureFlags';
|
|
5
8
|
|
|
6
9
|
interface MessageProps {
|
|
7
10
|
id: string;
|
|
@@ -9,13 +12,22 @@ interface MessageProps {
|
|
|
9
12
|
content: string;
|
|
10
13
|
timestamp: number;
|
|
11
14
|
isStreaming?: boolean;
|
|
15
|
+
diffData?: UnifiedFilesDiff;
|
|
16
|
+
beforeFiles?: any;
|
|
17
|
+
chatId?: string;
|
|
18
|
+
canUndo?: boolean;
|
|
12
19
|
}
|
|
13
20
|
|
|
14
21
|
const MessageComponent = ({
|
|
22
|
+
id,
|
|
15
23
|
role,
|
|
16
24
|
content,
|
|
17
25
|
timestamp,
|
|
18
26
|
isStreaming,
|
|
27
|
+
diffData,
|
|
28
|
+
beforeFiles,
|
|
29
|
+
chatId,
|
|
30
|
+
canUndo,
|
|
19
31
|
}: MessageProps) => {
|
|
20
32
|
// Memoize date formatting to avoid repeated computation
|
|
21
33
|
const formattedTime = useMemo(() => {
|
|
@@ -32,12 +44,24 @@ const MessageComponent = ({
|
|
|
32
44
|
const messageClassName = useMemo(() => {
|
|
33
45
|
return `ai-chat-message ${role}${isStreaming ? ' streaming' : ''}`;
|
|
34
46
|
}, [role, isStreaming]);
|
|
47
|
+
|
|
35
48
|
return (
|
|
36
49
|
<div className={messageClassName}>
|
|
37
50
|
<div className="ai-chat-message-content">
|
|
38
51
|
<Markdown remarkPlugins={[remarkGfm]}>
|
|
39
52
|
{content}
|
|
40
53
|
</Markdown>
|
|
54
|
+
{enableDiffView &&
|
|
55
|
+
diffData &&
|
|
56
|
+
Object.keys(diffData).length > 0 && (
|
|
57
|
+
<DiffView
|
|
58
|
+
diffData={diffData}
|
|
59
|
+
messageId={id}
|
|
60
|
+
chatId={chatId}
|
|
61
|
+
beforeFiles={beforeFiles}
|
|
62
|
+
canUndo={canUndo}
|
|
63
|
+
/>
|
|
64
|
+
)}
|
|
41
65
|
</div>
|
|
42
66
|
<div className="ai-chat-message-time">
|
|
43
67
|
{formattedTime}
|