more-compute 0.4.3__py3-none-any.whl → 0.5.0__py3-none-any.whl
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.
- frontend/app/globals.css +734 -27
- frontend/app/layout.tsx +13 -3
- frontend/components/Notebook.tsx +2 -14
- frontend/components/cell/MonacoCell.tsx +99 -5
- frontend/components/layout/Sidebar.tsx +39 -4
- frontend/components/panels/ClaudePanel.tsx +461 -0
- frontend/components/popups/ComputePopup.tsx +739 -418
- frontend/components/popups/FilterPopup.tsx +305 -189
- frontend/components/popups/MetricsPopup.tsx +20 -1
- frontend/components/popups/ProviderConfigModal.tsx +322 -0
- frontend/components/popups/ProviderDropdown.tsx +398 -0
- frontend/components/popups/SettingsPopup.tsx +1 -1
- frontend/contexts/ClaudeContext.tsx +392 -0
- frontend/contexts/PodWebSocketContext.tsx +16 -21
- frontend/hooks/useInlineDiff.ts +269 -0
- frontend/lib/api.ts +323 -12
- frontend/lib/settings.ts +5 -0
- frontend/lib/websocket-native.ts +4 -8
- frontend/lib/websocket.ts +1 -2
- frontend/package-lock.json +733 -36
- frontend/package.json +2 -0
- frontend/public/assets/icons/providers/lambda_labs.svg +22 -0
- frontend/public/assets/icons/providers/prime_intellect.svg +18 -0
- frontend/public/assets/icons/providers/runpod.svg +9 -0
- frontend/public/assets/icons/providers/vastai.svg +1 -0
- frontend/settings.md +54 -0
- frontend/tsconfig.tsbuildinfo +1 -0
- frontend/types/claude.ts +194 -0
- kernel_run.py +13 -0
- {more_compute-0.4.3.dist-info → more_compute-0.5.0.dist-info}/METADATA +53 -11
- {more_compute-0.4.3.dist-info → more_compute-0.5.0.dist-info}/RECORD +56 -37
- {more_compute-0.4.3.dist-info → more_compute-0.5.0.dist-info}/WHEEL +1 -1
- morecompute/__init__.py +1 -1
- morecompute/__version__.py +1 -1
- morecompute/execution/executor.py +24 -67
- morecompute/execution/worker.py +6 -72
- morecompute/models/api_models.py +62 -0
- morecompute/notebook.py +11 -0
- morecompute/server.py +641 -133
- morecompute/services/claude_service.py +392 -0
- morecompute/services/pod_manager.py +168 -67
- morecompute/services/pod_monitor.py +67 -39
- morecompute/services/prime_intellect.py +0 -4
- morecompute/services/providers/__init__.py +92 -0
- morecompute/services/providers/base_provider.py +336 -0
- morecompute/services/providers/lambda_labs_provider.py +394 -0
- morecompute/services/providers/provider_factory.py +194 -0
- morecompute/services/providers/runpod_provider.py +504 -0
- morecompute/services/providers/vastai_provider.py +407 -0
- morecompute/utils/cell_magics.py +0 -3
- morecompute/utils/config_util.py +93 -3
- morecompute/utils/special_commands.py +5 -32
- morecompute/utils/version_check.py +117 -0
- frontend/styling_README.md +0 -23
- {more_compute-0.4.3.dist-info/licenses → more_compute-0.5.0.dist-info}/LICENSE +0 -0
- {more_compute-0.4.3.dist-info → more_compute-0.5.0.dist-info}/entry_points.txt +0 -0
- {more_compute-0.4.3.dist-info → more_compute-0.5.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useState, useRef, useEffect } from "react";
|
|
4
|
+
import { useClaude } from "@/contexts/ClaudeContext";
|
|
5
|
+
import {
|
|
6
|
+
X,
|
|
7
|
+
Send,
|
|
8
|
+
Trash2,
|
|
9
|
+
Sparkles,
|
|
10
|
+
Check,
|
|
11
|
+
XCircle,
|
|
12
|
+
Key,
|
|
13
|
+
Copy,
|
|
14
|
+
CheckCheck,
|
|
15
|
+
} from "lucide-react";
|
|
16
|
+
import type { ClaudeMessage, ProposedEdit, ClaudeModel } from "@/types/claude";
|
|
17
|
+
|
|
18
|
+
// Simple markdown renderer for Claude messages
|
|
19
|
+
const renderMarkdown = (text: string): string => {
|
|
20
|
+
let html = text;
|
|
21
|
+
|
|
22
|
+
// Escape HTML first (but preserve our markdown)
|
|
23
|
+
const escapeHtml = (str: string) => {
|
|
24
|
+
return str
|
|
25
|
+
.replace(/&/g, "&")
|
|
26
|
+
.replace(/</g, "<")
|
|
27
|
+
.replace(/>/g, ">");
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Extract and preserve code blocks first
|
|
31
|
+
const codeBlocks: string[] = [];
|
|
32
|
+
// Pattern matches ```language\n or ```edit:N\n (any non-newline chars for identifier)
|
|
33
|
+
html = html.replace(/```([^\n]*)\n([\s\S]*?)```/g, (_, lang, code) => {
|
|
34
|
+
const index = codeBlocks.length;
|
|
35
|
+
const trimmedLang = lang.trim();
|
|
36
|
+
const langClass = trimmedLang
|
|
37
|
+
? ` class="language-${trimmedLang.replace(/[^a-zA-Z0-9_-]/g, "-")}"`
|
|
38
|
+
: "";
|
|
39
|
+
// Display "edit" for edit blocks, otherwise show the language
|
|
40
|
+
const displayLang = trimmedLang.startsWith("edit:")
|
|
41
|
+
? `edit (cell ${trimmedLang.split(":")[1]})`
|
|
42
|
+
: trimmedLang || "code";
|
|
43
|
+
codeBlocks.push(
|
|
44
|
+
`<div class="claude-code-block"><div class="claude-code-header"><span class="claude-code-lang">${escapeHtml(displayLang)}</span></div><pre><code${langClass}>${escapeHtml(code.trim())}</code></pre></div>`,
|
|
45
|
+
);
|
|
46
|
+
return `__CODE_BLOCK_${index}__`;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Simple code blocks without language
|
|
50
|
+
html = html.replace(/```([\s\S]*?)```/g, (_, code) => {
|
|
51
|
+
const index = codeBlocks.length;
|
|
52
|
+
codeBlocks.push(
|
|
53
|
+
`<div class="claude-code-block"><pre><code>${escapeHtml(code.trim())}</code></pre></div>`,
|
|
54
|
+
);
|
|
55
|
+
return `__CODE_BLOCK_${index}__`;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Headers
|
|
59
|
+
html = html.replace(/^### (.*$)/gim, "<h3>$1</h3>");
|
|
60
|
+
html = html.replace(/^## (.*$)/gim, "<h2>$1</h2>");
|
|
61
|
+
html = html.replace(/^# (.*$)/gim, "<h1>$1</h1>");
|
|
62
|
+
|
|
63
|
+
// Bold and Italic
|
|
64
|
+
html = html.replace(/\*\*\*([^*]+)\*\*\*/g, "<strong><em>$1</em></strong>");
|
|
65
|
+
html = html.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
|
66
|
+
html = html.replace(/\*([^*]+)\*/g, "<em>$1</em>");
|
|
67
|
+
|
|
68
|
+
// Inline code
|
|
69
|
+
html = html.replace(
|
|
70
|
+
/`([^`]+)`/g,
|
|
71
|
+
'<code class="claude-inline-code">$1</code>',
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// Links
|
|
75
|
+
html = html.replace(
|
|
76
|
+
/\[([^\]]+)\]\(([^)]+)\)/g,
|
|
77
|
+
'<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>',
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
// Lists - unordered
|
|
81
|
+
html = html.replace(/^[\s]*[-*] (.+)$/gim, "<li>$1</li>");
|
|
82
|
+
|
|
83
|
+
// Lists - ordered
|
|
84
|
+
html = html.replace(/^[\s]*\d+\. (.+)$/gim, "<li>$1</li>");
|
|
85
|
+
|
|
86
|
+
// Wrap consecutive li elements in ul/ol
|
|
87
|
+
html = html.replace(/(<li>[\s\S]*?<\/li>)+/g, (match) => `<ul>${match}</ul>`);
|
|
88
|
+
|
|
89
|
+
// Line breaks - double newline = paragraph, single = br
|
|
90
|
+
html = html.replace(/\n\n+/g, "</p><p>");
|
|
91
|
+
html = html.replace(/\n/g, "<br>");
|
|
92
|
+
|
|
93
|
+
// Wrap in paragraph if needed
|
|
94
|
+
if (!html.startsWith("<") && html.trim()) {
|
|
95
|
+
html = "<p>" + html + "</p>";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Restore code blocks
|
|
99
|
+
codeBlocks.forEach((block, index) => {
|
|
100
|
+
html = html.replace(`__CODE_BLOCK_${index}__`, block);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Clean up empty paragraphs
|
|
104
|
+
html = html.replace(/<p>\s*<\/p>/g, "");
|
|
105
|
+
html = html.replace(/<p><br><\/p>/g, "");
|
|
106
|
+
|
|
107
|
+
return html;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// Model selector component
|
|
111
|
+
interface ModelSelectorProps {
|
|
112
|
+
model: ClaudeModel;
|
|
113
|
+
onChange: (model: ClaudeModel) => void;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const MODEL_OPTIONS: { value: ClaudeModel; label: string }[] = [
|
|
117
|
+
{ value: "haiku", label: "Haiku 4" },
|
|
118
|
+
{ value: "sonnet", label: "Sonnet 4" },
|
|
119
|
+
{ value: "opus", label: "Opus 4.5" },
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
const ModelSelector: React.FC<ModelSelectorProps> = ({ model, onChange }) => {
|
|
123
|
+
return (
|
|
124
|
+
<div className="claude-model-selector">
|
|
125
|
+
{MODEL_OPTIONS.map((option) => (
|
|
126
|
+
<button
|
|
127
|
+
key={option.value}
|
|
128
|
+
className={`claude-model-option ${model === option.value ? "active" : ""}`}
|
|
129
|
+
onClick={() => onChange(option.value)}
|
|
130
|
+
title={
|
|
131
|
+
option.value === "haiku"
|
|
132
|
+
? "Fast & cheap"
|
|
133
|
+
: option.value === "opus"
|
|
134
|
+
? "Most capable"
|
|
135
|
+
: "Balanced"
|
|
136
|
+
}
|
|
137
|
+
>
|
|
138
|
+
{option.label}
|
|
139
|
+
</button>
|
|
140
|
+
))}
|
|
141
|
+
</div>
|
|
142
|
+
);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const ClaudePanel: React.FC = () => {
|
|
146
|
+
const {
|
|
147
|
+
messages,
|
|
148
|
+
isPanelOpen,
|
|
149
|
+
isLoading,
|
|
150
|
+
isConfigured,
|
|
151
|
+
error,
|
|
152
|
+
model,
|
|
153
|
+
sendMessage,
|
|
154
|
+
applyEdit,
|
|
155
|
+
rejectEdit,
|
|
156
|
+
closePanel,
|
|
157
|
+
clearHistory,
|
|
158
|
+
setApiKey,
|
|
159
|
+
setModel,
|
|
160
|
+
} = useClaude();
|
|
161
|
+
|
|
162
|
+
const [inputValue, setInputValue] = useState("");
|
|
163
|
+
const [apiKeyInput, setApiKeyInput] = useState("");
|
|
164
|
+
const [isSettingKey, setIsSettingKey] = useState(false);
|
|
165
|
+
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
166
|
+
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
167
|
+
|
|
168
|
+
// Auto-scroll to bottom when new messages arrive
|
|
169
|
+
useEffect(() => {
|
|
170
|
+
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
171
|
+
}, [messages]);
|
|
172
|
+
|
|
173
|
+
// Focus input when panel opens
|
|
174
|
+
useEffect(() => {
|
|
175
|
+
if (isPanelOpen && inputRef.current) {
|
|
176
|
+
inputRef.current.focus();
|
|
177
|
+
}
|
|
178
|
+
}, [isPanelOpen]);
|
|
179
|
+
|
|
180
|
+
const handleSend = () => {
|
|
181
|
+
if (!inputValue.trim() || isLoading) return;
|
|
182
|
+
sendMessage(inputValue);
|
|
183
|
+
setInputValue("");
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
187
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
188
|
+
e.preventDefault();
|
|
189
|
+
handleSend();
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const handleSetApiKey = async () => {
|
|
194
|
+
if (!apiKeyInput.trim()) return;
|
|
195
|
+
setIsSettingKey(true);
|
|
196
|
+
const success = await setApiKey(apiKeyInput);
|
|
197
|
+
if (success) {
|
|
198
|
+
setApiKeyInput("");
|
|
199
|
+
}
|
|
200
|
+
setIsSettingKey(false);
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
if (!isPanelOpen) return null;
|
|
204
|
+
|
|
205
|
+
return (
|
|
206
|
+
<div className="claude-panel">
|
|
207
|
+
{/* Header */}
|
|
208
|
+
<div className="claude-panel-header">
|
|
209
|
+
<div className="claude-panel-title">
|
|
210
|
+
<Sparkles size={16} />
|
|
211
|
+
<span>Claude</span>
|
|
212
|
+
</div>
|
|
213
|
+
<div className="claude-panel-actions">
|
|
214
|
+
<button
|
|
215
|
+
className="claude-panel-btn"
|
|
216
|
+
onClick={clearHistory}
|
|
217
|
+
title="Clear history"
|
|
218
|
+
>
|
|
219
|
+
<Trash2 size={14} />
|
|
220
|
+
</button>
|
|
221
|
+
<button
|
|
222
|
+
className="claude-panel-btn"
|
|
223
|
+
onClick={closePanel}
|
|
224
|
+
title="Close"
|
|
225
|
+
>
|
|
226
|
+
<X size={14} />
|
|
227
|
+
</button>
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
|
|
231
|
+
{/* Error Banner */}
|
|
232
|
+
{error && (
|
|
233
|
+
<div className="claude-error-banner">
|
|
234
|
+
<XCircle size={14} />
|
|
235
|
+
<span>{error}</span>
|
|
236
|
+
</div>
|
|
237
|
+
)}
|
|
238
|
+
|
|
239
|
+
{/* Messages */}
|
|
240
|
+
<div className="claude-messages">
|
|
241
|
+
{messages.length === 0 ? (
|
|
242
|
+
<div className="claude-empty-state">
|
|
243
|
+
<Sparkles size={24} />
|
|
244
|
+
<p>Ask Claude to help with your code</p>
|
|
245
|
+
<p className="claude-empty-hint">this is a work in progress</p>
|
|
246
|
+
</div>
|
|
247
|
+
) : (
|
|
248
|
+
messages.map((message) => (
|
|
249
|
+
<MessageBubble
|
|
250
|
+
key={message.id}
|
|
251
|
+
message={message}
|
|
252
|
+
onApplyEdit={applyEdit}
|
|
253
|
+
onRejectEdit={rejectEdit}
|
|
254
|
+
/>
|
|
255
|
+
))
|
|
256
|
+
)}
|
|
257
|
+
<div ref={messagesEndRef} />
|
|
258
|
+
</div>
|
|
259
|
+
|
|
260
|
+
{/* API Key Config */}
|
|
261
|
+
{!isConfigured && (
|
|
262
|
+
<div className="claude-config-banner">
|
|
263
|
+
<div className="claude-config-title">
|
|
264
|
+
<Key size={14} />
|
|
265
|
+
<span>Configure your Claude API key to get started</span>
|
|
266
|
+
</div>
|
|
267
|
+
<div className="claude-config-form">
|
|
268
|
+
<input
|
|
269
|
+
type="password"
|
|
270
|
+
value={apiKeyInput}
|
|
271
|
+
onChange={(e) => setApiKeyInput(e.target.value)}
|
|
272
|
+
placeholder="sk-ant-..."
|
|
273
|
+
className="claude-config-input"
|
|
274
|
+
onKeyDown={(e) => e.key === "Enter" && handleSetApiKey()}
|
|
275
|
+
/>
|
|
276
|
+
<button
|
|
277
|
+
onClick={handleSetApiKey}
|
|
278
|
+
disabled={isSettingKey || !apiKeyInput.trim()}
|
|
279
|
+
className="claude-config-btn"
|
|
280
|
+
>
|
|
281
|
+
{isSettingKey ? "Saving..." : "Save"}
|
|
282
|
+
</button>
|
|
283
|
+
</div>
|
|
284
|
+
</div>
|
|
285
|
+
)}
|
|
286
|
+
|
|
287
|
+
{/* Input */}
|
|
288
|
+
<div className="claude-input-container">
|
|
289
|
+
<textarea
|
|
290
|
+
ref={inputRef}
|
|
291
|
+
value={inputValue}
|
|
292
|
+
onChange={(e) => setInputValue(e.target.value)}
|
|
293
|
+
onKeyDown={handleKeyDown}
|
|
294
|
+
placeholder={
|
|
295
|
+
isConfigured ? "Ask Claude..." : "Configure API key above"
|
|
296
|
+
}
|
|
297
|
+
disabled={!isConfigured || isLoading}
|
|
298
|
+
className="claude-input"
|
|
299
|
+
rows={1}
|
|
300
|
+
/>
|
|
301
|
+
<button
|
|
302
|
+
onClick={handleSend}
|
|
303
|
+
disabled={!isConfigured || isLoading || !inputValue.trim()}
|
|
304
|
+
className="claude-send-btn"
|
|
305
|
+
>
|
|
306
|
+
{isLoading ? (
|
|
307
|
+
<div className="claude-loading-spinner" />
|
|
308
|
+
) : (
|
|
309
|
+
<Send size={16} />
|
|
310
|
+
)}
|
|
311
|
+
</button>
|
|
312
|
+
</div>
|
|
313
|
+
|
|
314
|
+
{/* Model Selector */}
|
|
315
|
+
<div className="claude-model-row">
|
|
316
|
+
<span className="claude-model-label">Model:</span>
|
|
317
|
+
<ModelSelector model={model} onChange={setModel} />
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
);
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
interface MessageBubbleProps {
|
|
324
|
+
message: ClaudeMessage;
|
|
325
|
+
onApplyEdit: (editId: string) => void;
|
|
326
|
+
onRejectEdit: (editId: string) => void;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const MessageBubble: React.FC<MessageBubbleProps> = ({
|
|
330
|
+
message,
|
|
331
|
+
onApplyEdit,
|
|
332
|
+
onRejectEdit,
|
|
333
|
+
}) => {
|
|
334
|
+
const isUser = message.role === "user";
|
|
335
|
+
|
|
336
|
+
// Format the message content, removing edit blocks for display
|
|
337
|
+
const formatContent = (content: string) => {
|
|
338
|
+
// Remove edit blocks from display
|
|
339
|
+
return content.replace(/```edit:\d+\n[\s\S]*?```/g, "").trim();
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const formattedContent = formatContent(message.content);
|
|
343
|
+
|
|
344
|
+
return (
|
|
345
|
+
<div className={`claude-message ${isUser ? "user" : "assistant"}`}>
|
|
346
|
+
<div className="claude-message-content">
|
|
347
|
+
{message.isStreaming ? (
|
|
348
|
+
<div className="claude-streaming">
|
|
349
|
+
<div
|
|
350
|
+
className="claude-message-text"
|
|
351
|
+
dangerouslySetInnerHTML={{
|
|
352
|
+
__html: renderMarkdown(formattedContent) || "Thinking...",
|
|
353
|
+
}}
|
|
354
|
+
/>
|
|
355
|
+
<span className="claude-cursor">|</span>
|
|
356
|
+
</div>
|
|
357
|
+
) : (
|
|
358
|
+
<>
|
|
359
|
+
{isUser ? (
|
|
360
|
+
<div className="claude-message-text">{formattedContent}</div>
|
|
361
|
+
) : (
|
|
362
|
+
<div
|
|
363
|
+
className="claude-message-text claude-markdown"
|
|
364
|
+
dangerouslySetInnerHTML={{
|
|
365
|
+
__html: renderMarkdown(formattedContent),
|
|
366
|
+
}}
|
|
367
|
+
/>
|
|
368
|
+
)}
|
|
369
|
+
{/* Proposed Edits */}
|
|
370
|
+
{message.proposedEdits && message.proposedEdits.length > 0 && (
|
|
371
|
+
<div className="claude-edits">
|
|
372
|
+
{message.proposedEdits.map((edit) => (
|
|
373
|
+
<EditCard
|
|
374
|
+
key={edit.id}
|
|
375
|
+
edit={edit}
|
|
376
|
+
onApply={() => onApplyEdit(edit.id)}
|
|
377
|
+
onReject={() => onRejectEdit(edit.id)}
|
|
378
|
+
/>
|
|
379
|
+
))}
|
|
380
|
+
</div>
|
|
381
|
+
)}
|
|
382
|
+
</>
|
|
383
|
+
)}
|
|
384
|
+
</div>
|
|
385
|
+
</div>
|
|
386
|
+
);
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
interface EditCardProps {
|
|
390
|
+
edit: ProposedEdit;
|
|
391
|
+
onApply: () => void;
|
|
392
|
+
onReject: () => void;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const EditCard: React.FC<EditCardProps> = ({ edit, onApply, onReject }) => {
|
|
396
|
+
const [copied, setCopied] = useState(false);
|
|
397
|
+
const isPending = edit.status === "pending";
|
|
398
|
+
const isApplied = edit.status === "applied";
|
|
399
|
+
const isRejected = edit.status === "rejected";
|
|
400
|
+
|
|
401
|
+
const handleCopy = async () => {
|
|
402
|
+
await navigator.clipboard.writeText(edit.newCode);
|
|
403
|
+
setCopied(true);
|
|
404
|
+
setTimeout(() => setCopied(false), 2000);
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
return (
|
|
408
|
+
<div
|
|
409
|
+
className={`claude-edit-card ${isApplied ? "applied" : ""} ${isRejected ? "rejected" : ""}`}
|
|
410
|
+
>
|
|
411
|
+
<div className="claude-edit-header">
|
|
412
|
+
<span className="claude-edit-cell">Cell {edit.cellIndex}</span>
|
|
413
|
+
<div className="claude-edit-actions">
|
|
414
|
+
<button
|
|
415
|
+
className="claude-edit-btn copy"
|
|
416
|
+
onClick={handleCopy}
|
|
417
|
+
title="Copy code"
|
|
418
|
+
>
|
|
419
|
+
{copied ? <CheckCheck size={12} /> : <Copy size={12} />}
|
|
420
|
+
</button>
|
|
421
|
+
{isPending && (
|
|
422
|
+
<>
|
|
423
|
+
<button
|
|
424
|
+
className="claude-edit-btn accept"
|
|
425
|
+
onClick={onApply}
|
|
426
|
+
title="Apply edit"
|
|
427
|
+
>
|
|
428
|
+
<Check size={12} />
|
|
429
|
+
Apply
|
|
430
|
+
</button>
|
|
431
|
+
<button
|
|
432
|
+
className="claude-edit-btn reject"
|
|
433
|
+
onClick={onReject}
|
|
434
|
+
title="Reject edit"
|
|
435
|
+
>
|
|
436
|
+
<X size={12} />
|
|
437
|
+
Reject
|
|
438
|
+
</button>
|
|
439
|
+
</>
|
|
440
|
+
)}
|
|
441
|
+
</div>
|
|
442
|
+
{isApplied && (
|
|
443
|
+
<span className="claude-edit-status applied">Applied</span>
|
|
444
|
+
)}
|
|
445
|
+
{isRejected && (
|
|
446
|
+
<span className="claude-edit-status rejected">Rejected</span>
|
|
447
|
+
)}
|
|
448
|
+
</div>
|
|
449
|
+
{edit.explanation && (
|
|
450
|
+
<div className="claude-edit-explanation">{edit.explanation}</div>
|
|
451
|
+
)}
|
|
452
|
+
<div className="claude-edit-preview">
|
|
453
|
+
<pre className="claude-edit-code">
|
|
454
|
+
<code>{edit.newCode}</code>
|
|
455
|
+
</pre>
|
|
456
|
+
</div>
|
|
457
|
+
</div>
|
|
458
|
+
);
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
export default ClaudePanel;
|