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.
Files changed (42) hide show
  1. package/dist/assets/{buildWorker-CRLuRnCr.js → buildWorker-C_tGhWXG.js} +135 -74
  2. package/dist/assets/index-CC0CUUDi.js +457 -0
  3. package/dist/assets/{index-Beqaj63x.css → index-CQyHWTWE.css} +1 -1
  4. package/dist/assets/worker-CekYspLa.js +453 -0
  5. package/dist/index.html +2 -2
  6. package/package.json +13 -7
  7. package/src/client/CodeEditor/{getOrCreateEditor.ts → getOrCreateEditor.tsx} +129 -1
  8. package/src/client/CodeEditor/index.tsx +11 -4
  9. package/src/client/Icons/SparklesSVG.tsx +9 -3
  10. package/src/client/RunCodeWidget/index.tsx +18 -15
  11. package/src/client/SplitPaneResizeContext.tsx +26 -108
  12. package/src/client/VZCodeContext.tsx +15 -1
  13. package/src/client/VZRight.tsx +32 -4
  14. package/src/client/VZSidebar/AIChat/ChatInput.tsx +80 -10
  15. package/src/client/VZSidebar/AIChat/DiffView.scss +154 -0
  16. package/src/client/VZSidebar/AIChat/DiffView.tsx +133 -0
  17. package/src/client/VZSidebar/AIChat/Message.tsx +24 -0
  18. package/src/client/VZSidebar/AIChat/MessageList.tsx +134 -12
  19. package/src/client/VZSidebar/AIChat/ThinkingScratchpad.tsx +37 -0
  20. package/src/client/VZSidebar/AIChat/index.tsx +155 -17
  21. package/src/client/VZSidebar/AIChat/styles.scss +223 -1
  22. package/src/client/VZSidebar/aiCopyPaste.ts +15 -2
  23. package/src/client/VZSidebar/index.tsx +10 -2
  24. package/src/client/bootstrap.ts +11 -1
  25. package/src/client/featureFlags.ts +2 -0
  26. package/src/client/useActions.ts +12 -0
  27. package/src/client/usePrettier/index.ts +1 -3
  28. package/src/client/vzReducer/aiChatReducer.ts +13 -0
  29. package/src/client/vzReducer/createInitialState.ts +1 -0
  30. package/src/client/vzReducer/index.ts +9 -0
  31. package/src/client/vzReducer/searchReducer.test.ts +44 -2
  32. package/src/runCode.ts +5 -17
  33. package/src/server/aiChatHandler/aiEditing.ts +56 -0
  34. package/src/server/aiChatHandler/chatOperations.ts +78 -1
  35. package/src/server/aiChatHandler/index.ts +33 -9
  36. package/src/server/aiChatHandler/llmStreaming.ts +101 -19
  37. package/src/server/computeInitialDocument.ts +25 -6
  38. package/src/server/generateAIResponse.ts +4 -0
  39. package/src/server/index.ts +83 -2
  40. package/src/utils/fileDiff.ts +114 -0
  41. package/dist/assets/index-C46Hp5ym.js +0 -340
  42. package/dist/assets/worker-DkYJN5WQ.js +0 -453
@@ -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
- if (isFirstRunRef.current || isInteracting) {
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
- enableHotReloading: true,
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 { Form, Button } from '../../bootstrap';
7
+ import {
8
+ Form,
9
+ Button,
10
+ ButtonGroup,
11
+ ToggleButton,
12
+ } from '../../bootstrap';
8
13
 
9
14
  interface ChatInputProps {
10
- message: string;
11
- setMessage: (message: string) => void;
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
- message,
19
- setMessage,
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
- setMessage(event.target.value);
54
+ setAIChatMessage(event.target.value);
46
55
  },
47
- [setMessage],
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={message}
109
+ value={aiChatMessage}
57
110
  onChange={handleChange}
58
111
  onKeyDown={handleKeyDown}
59
112
  ref={inputRef}
60
- placeholder="Describe what you'd like me to change in your code..."
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={!message.trim() || isLoading}
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}