nitrostack 1.0.16 → 1.0.17
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/core/app-decorator.js +2 -2
- package/dist/core/app-decorator.js.map +1 -1
- package/dist/core/builders.js +2 -2
- package/dist/core/builders.js.map +1 -1
- package/dist/core/resource.js +1 -1
- package/dist/core/resource.js.map +1 -1
- package/dist/core/server.js +2 -2
- package/dist/core/server.js.map +1 -1
- package/dist/core/types.d.ts +1 -1
- package/dist/core/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/studio/app/api/chat/route.ts +151 -35
- package/src/studio/app/chat/page.tsx +86 -31
- package/src/studio/app/layout.tsx +1 -1
- package/src/studio/components/EnlargeModal.tsx +21 -15
- package/src/studio/components/LogMessage.tsx +153 -0
- package/src/studio/components/MarkdownRenderer.tsx +410 -0
- package/src/studio/components/Sidebar.tsx +3 -3
- package/src/studio/components/ToolCard.tsx +27 -9
- package/src/studio/components/WidgetRenderer.tsx +4 -2
- package/src/studio/lib/llm-service.ts +23 -1
|
@@ -4,6 +4,7 @@ import { useEffect, useRef, useState } from 'react';
|
|
|
4
4
|
import { useStudioStore } from '@/lib/store';
|
|
5
5
|
import { api } from '@/lib/api';
|
|
6
6
|
import { WidgetRenderer } from '@/components/WidgetRenderer';
|
|
7
|
+
import { MarkdownRenderer } from '@/components/MarkdownRenderer';
|
|
7
8
|
import type { ChatMessage, Tool, ToolCall, Prompt } from '@/lib/types';
|
|
8
9
|
import {
|
|
9
10
|
Bot,
|
|
@@ -58,6 +59,17 @@ export default function ChatPage() {
|
|
|
58
59
|
useEffect(() => {
|
|
59
60
|
loadTools();
|
|
60
61
|
loadPrompts();
|
|
62
|
+
|
|
63
|
+
// Check if there's a suggested message from localStorage
|
|
64
|
+
if (typeof window !== 'undefined') {
|
|
65
|
+
const chatInput = window.localStorage.getItem('chatInput');
|
|
66
|
+
if (chatInput) {
|
|
67
|
+
setInputValue(chatInput);
|
|
68
|
+
window.localStorage.removeItem('chatInput');
|
|
69
|
+
// Focus after a short delay to ensure component is mounted
|
|
70
|
+
setTimeout(() => textareaRef.current?.focus(), 100);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
61
73
|
}, []);
|
|
62
74
|
|
|
63
75
|
useEffect(() => {
|
|
@@ -101,23 +113,61 @@ export default function ChatPage() {
|
|
|
101
113
|
if (!selectedPrompt) return;
|
|
102
114
|
|
|
103
115
|
// Close modal
|
|
116
|
+
const prompt = selectedPrompt;
|
|
117
|
+
const args = { ...promptArgs };
|
|
104
118
|
setSelectedPrompt(null);
|
|
119
|
+
setPromptArgs({});
|
|
105
120
|
|
|
106
|
-
// Build
|
|
107
|
-
let
|
|
108
|
-
if (Object.keys(
|
|
109
|
-
|
|
121
|
+
// Build user message showing what prompt was executed
|
|
122
|
+
let userMessageContent = `Execute prompt: ${prompt.name}`;
|
|
123
|
+
if (Object.keys(args).length > 0) {
|
|
124
|
+
userMessageContent += `\nArguments: ${JSON.stringify(args, null, 2)}`;
|
|
110
125
|
}
|
|
111
126
|
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
|
|
127
|
+
// Add user message to chat
|
|
128
|
+
addChatMessage({
|
|
129
|
+
role: 'user',
|
|
130
|
+
content: userMessageContent,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
setLoading(true);
|
|
115
134
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
135
|
+
try {
|
|
136
|
+
// Execute the prompt directly via API
|
|
137
|
+
const result = await api.executePrompt(prompt.name, args);
|
|
138
|
+
|
|
139
|
+
// Add the prompt result as an assistant message
|
|
140
|
+
if (result.messages && result.messages.length > 0) {
|
|
141
|
+
// Combine all prompt messages into one assistant message
|
|
142
|
+
const combinedContent = result.messages
|
|
143
|
+
.map((msg: any) => {
|
|
144
|
+
const content = typeof msg.content === 'string'
|
|
145
|
+
? msg.content
|
|
146
|
+
: msg.content?.text || JSON.stringify(msg.content);
|
|
147
|
+
return `[${msg.role.toUpperCase()}]\n${content}`;
|
|
148
|
+
})
|
|
149
|
+
.join('\n\n');
|
|
150
|
+
|
|
151
|
+
addChatMessage({
|
|
152
|
+
role: 'assistant',
|
|
153
|
+
content: combinedContent,
|
|
154
|
+
});
|
|
155
|
+
} else {
|
|
156
|
+
addChatMessage({
|
|
157
|
+
role: 'assistant',
|
|
158
|
+
content: 'Prompt executed successfully, but returned no messages.',
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
} catch (error) {
|
|
162
|
+
console.error('Failed to execute prompt:', error);
|
|
163
|
+
addChatMessage({
|
|
164
|
+
role: 'assistant',
|
|
165
|
+
content: `Error executing prompt: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
166
|
+
});
|
|
167
|
+
} finally {
|
|
168
|
+
setLoading(false);
|
|
169
|
+
setTimeout(() => textareaRef.current?.focus(), 100);
|
|
170
|
+
}
|
|
121
171
|
};
|
|
122
172
|
|
|
123
173
|
const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
@@ -373,7 +423,7 @@ export default function ChatPage() {
|
|
|
373
423
|
return (
|
|
374
424
|
<div className="fixed inset-0 flex flex-col bg-background" style={{ left: 'var(--sidebar-width, 15rem)' }}>
|
|
375
425
|
{/* Sticky Header */}
|
|
376
|
-
<div className="sticky top-0 z-10 border-b border-border/50 px-6 py-3 flex items-center justify-between bg-card/80 backdrop-blur-md shadow-sm">
|
|
426
|
+
<div className="sticky top-0 z-10 border-b border-border/50 px-3 sm:px-6 py-3 flex flex-col sm:flex-row items-start sm:items-center justify-between bg-card/80 backdrop-blur-md shadow-sm gap-3 sm:gap-0">
|
|
377
427
|
<div className="flex items-center gap-3">
|
|
378
428
|
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-primary to-amber-500 flex items-center justify-center shadow-md">
|
|
379
429
|
<Bot className="w-5 h-5 text-white" strokeWidth={2.5} />
|
|
@@ -383,18 +433,18 @@ export default function ChatPage() {
|
|
|
383
433
|
</div>
|
|
384
434
|
</div>
|
|
385
435
|
|
|
386
|
-
<div className="flex items-center gap-2">
|
|
436
|
+
<div className="flex items-center gap-2 w-full sm:w-auto">
|
|
387
437
|
<select
|
|
388
438
|
value={currentProvider}
|
|
389
439
|
onChange={(e) => setCurrentProvider(e.target.value as 'openai' | 'gemini')}
|
|
390
|
-
className="input text-sm px-3 py-1.5 w-28"
|
|
440
|
+
className="input text-sm px-3 py-1.5 w-full sm:w-28 flex-1 sm:flex-none"
|
|
391
441
|
>
|
|
392
442
|
<option value="gemini">Gemini</option>
|
|
393
443
|
<option value="openai">OpenAI</option>
|
|
394
444
|
</select>
|
|
395
445
|
<button
|
|
396
446
|
onClick={() => setShowSettings(!showSettings)}
|
|
397
|
-
className={`w-8 h-8 rounded-lg flex items-center justify-center transition-all ${
|
|
447
|
+
className={`w-8 h-8 rounded-lg flex items-center justify-center transition-all flex-shrink-0 ${
|
|
398
448
|
showSettings
|
|
399
449
|
? 'bg-primary/10 text-primary ring-1 ring-primary/30'
|
|
400
450
|
: 'bg-muted/50 text-muted-foreground hover:bg-muted hover:text-foreground'
|
|
@@ -405,7 +455,7 @@ export default function ChatPage() {
|
|
|
405
455
|
</button>
|
|
406
456
|
<button
|
|
407
457
|
onClick={clearChat}
|
|
408
|
-
className="w-8 h-8 rounded-lg flex items-center justify-center bg-muted/50 text-muted-foreground hover:bg-muted hover:text-foreground transition-all"
|
|
458
|
+
className="w-8 h-8 rounded-lg flex items-center justify-center bg-muted/50 text-muted-foreground hover:bg-muted hover:text-foreground transition-all flex-shrink-0"
|
|
409
459
|
title="Clear chat"
|
|
410
460
|
>
|
|
411
461
|
<Trash2 className="w-4 h-4" />
|
|
@@ -415,7 +465,7 @@ export default function ChatPage() {
|
|
|
415
465
|
|
|
416
466
|
{/* Enhanced Settings Panel */}
|
|
417
467
|
{showSettings && (
|
|
418
|
-
<div className="border-b border-border/50 px-6 py-5 bg-muted/20 backdrop-blur-md shadow-sm">
|
|
468
|
+
<div className="border-b border-border/50 px-3 sm:px-6 py-4 sm:py-5 bg-muted/20 backdrop-blur-md shadow-sm">
|
|
419
469
|
<div className="max-w-4xl mx-auto">
|
|
420
470
|
<div className="flex items-start justify-between mb-4">
|
|
421
471
|
<div>
|
|
@@ -427,7 +477,7 @@ export default function ChatPage() {
|
|
|
427
477
|
</div>
|
|
428
478
|
</div>
|
|
429
479
|
|
|
430
|
-
|
|
480
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
431
481
|
{/* OpenAI Section */}
|
|
432
482
|
<div className="card p-4">
|
|
433
483
|
<div className="flex items-center justify-between mb-3">
|
|
@@ -666,7 +716,7 @@ export default function ChatPage() {
|
|
|
666
716
|
|
|
667
717
|
{/* ChatGPT-style Input Area - Fixed at bottom */}
|
|
668
718
|
<div className="sticky bottom-0 border-t border-border/50 bg-background/95 backdrop-blur-md shadow-[0_-2px_10px_rgba(0,0,0,0.1)]">
|
|
669
|
-
<div className="max-w-3xl mx-auto px-4 py-4">
|
|
719
|
+
<div className="max-w-3xl mx-auto px-3 sm:px-4 py-3 sm:py-4">
|
|
670
720
|
{currentImage && (
|
|
671
721
|
<div className="mb-3 p-3 bg-card rounded-xl flex items-start gap-3 border border-border/50 animate-fade-in">
|
|
672
722
|
<img
|
|
@@ -732,9 +782,7 @@ export default function ChatPage() {
|
|
|
732
782
|
<Send className="w-5 h-5" strokeWidth={2.5} />
|
|
733
783
|
</button>
|
|
734
784
|
</div>
|
|
735
|
-
|
|
736
|
-
NitroStudio can make mistakes. Check important info.
|
|
737
|
-
</p>
|
|
785
|
+
|
|
738
786
|
</div>
|
|
739
787
|
</div>
|
|
740
788
|
|
|
@@ -803,10 +851,11 @@ export default function ChatPage() {
|
|
|
803
851
|
>
|
|
804
852
|
<Play className="w-4 h-4" />
|
|
805
853
|
Execute Prompt
|
|
806
|
-
|
|
807
|
-
</div>
|
|
854
|
+
</button>
|
|
808
855
|
</div>
|
|
856
|
+
|
|
809
857
|
</div>
|
|
858
|
+
</div>
|
|
810
859
|
)}
|
|
811
860
|
</div>
|
|
812
861
|
);
|
|
@@ -844,10 +893,14 @@ function ChatMessageComponent({ message, tools }: { message: ChatMessage; tools:
|
|
|
844
893
|
</div>
|
|
845
894
|
)}
|
|
846
895
|
|
|
847
|
-
{/* Text content */}
|
|
896
|
+
{/* Text content with markdown rendering */}
|
|
848
897
|
{message.content && (
|
|
849
|
-
<div className="text-sm
|
|
850
|
-
{
|
|
898
|
+
<div className="text-sm leading-relaxed mb-4">
|
|
899
|
+
{isUser ? (
|
|
900
|
+
<div className="whitespace-pre-wrap text-foreground/90">{message.content}</div>
|
|
901
|
+
) : (
|
|
902
|
+
<MarkdownRenderer content={message.content} />
|
|
903
|
+
)}
|
|
851
904
|
</div>
|
|
852
905
|
)}
|
|
853
906
|
|
|
@@ -927,11 +980,13 @@ function ToolCallComponent({ toolCall, tools }: { toolCall: ToolCall; tools: Too
|
|
|
927
980
|
|
|
928
981
|
{/* Widget - ChatGPT-style embedded card */}
|
|
929
982
|
{componentUri && widgetData && (
|
|
930
|
-
<div className="border-t border-border/30">
|
|
983
|
+
<div className="border-t border-border/30 pt-3 flex justify-center">
|
|
931
984
|
<div className="rounded-lg overflow-hidden bg-background" style={{
|
|
932
|
-
|
|
985
|
+
width: 'fit-content',
|
|
986
|
+
maxWidth: '100%',
|
|
987
|
+
height: '450px',
|
|
933
988
|
}}>
|
|
934
|
-
<WidgetRenderer uri={componentUri} data={widgetData} />
|
|
989
|
+
<WidgetRenderer uri={componentUri} data={widgetData} className="widget-in-chat" />
|
|
935
990
|
</div>
|
|
936
991
|
</div>
|
|
937
992
|
)}
|
|
@@ -40,7 +40,7 @@ export default function RootLayout({
|
|
|
40
40
|
<div className="flex min-h-screen bg-gradient-to-br from-background via-background to-muted/20">
|
|
41
41
|
<Sidebar />
|
|
42
42
|
<main className="flex-1 transition-all duration-300 ease-in-out" style={{ marginLeft: 'var(--sidebar-width, 15rem)' }}>
|
|
43
|
-
<div className="min-h-screen p-8">
|
|
43
|
+
<div className="min-h-screen p-3 sm:p-6 md:p-8">
|
|
44
44
|
{children}
|
|
45
45
|
</div>
|
|
46
46
|
</main>
|
|
@@ -26,13 +26,18 @@ export function EnlargeModal() {
|
|
|
26
26
|
if (type !== 'tool') return;
|
|
27
27
|
|
|
28
28
|
closeEnlargeModal();
|
|
29
|
-
setCurrentTab('chat');
|
|
30
|
-
router.push('/chat');
|
|
31
29
|
|
|
32
|
-
//
|
|
30
|
+
// Build the tool execution message
|
|
31
|
+
const toolMessage = `Use the ${item.name} tool`;
|
|
32
|
+
|
|
33
|
+
// Store both the tool name and the message
|
|
33
34
|
if (typeof window !== 'undefined') {
|
|
34
35
|
window.localStorage.setItem('suggestedTool', item.name);
|
|
36
|
+
window.localStorage.setItem('chatInput', toolMessage);
|
|
35
37
|
}
|
|
38
|
+
|
|
39
|
+
setCurrentTab('chat');
|
|
40
|
+
router.push('/chat');
|
|
36
41
|
};
|
|
37
42
|
|
|
38
43
|
return (
|
|
@@ -42,34 +47,35 @@ export function EnlargeModal() {
|
|
|
42
47
|
onClick={closeEnlargeModal}
|
|
43
48
|
>
|
|
44
49
|
<div
|
|
45
|
-
className="relative w-[
|
|
50
|
+
className="relative w-[95vw] max-w-7xl h-[90vh] bg-card border border-border rounded-xl sm:rounded-2xl shadow-2xl overflow-hidden animate-scale-in"
|
|
46
51
|
onClick={(e) => e.stopPropagation()}
|
|
47
52
|
>
|
|
48
53
|
{/* Header */}
|
|
49
|
-
<div className="flex items-center justify-between p-6 border-b border-border bg-muted/30">
|
|
50
|
-
<div className="flex items-center gap-4">
|
|
51
|
-
<span className="text-3xl">{type === 'tool' ? '⚡' : '🎨'}</span>
|
|
52
|
-
<div>
|
|
53
|
-
<h2 className="text-xl font-bold text-foreground">{item.name}</h2>
|
|
54
|
-
<p className="text-sm text-muted-foreground mt-1">
|
|
54
|
+
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between p-4 sm:p-6 border-b border-border bg-muted/30 gap-3 sm:gap-0">
|
|
55
|
+
<div className="flex items-start sm:items-center gap-3 sm:gap-4 flex-1 min-w-0">
|
|
56
|
+
<span className="text-2xl sm:text-3xl flex-shrink-0">{type === 'tool' ? '⚡' : '🎨'}</span>
|
|
57
|
+
<div className="min-w-0 flex-1">
|
|
58
|
+
<h2 className="text-lg sm:text-xl font-bold text-foreground truncate">{item.name}</h2>
|
|
59
|
+
<p className="text-xs sm:text-sm text-muted-foreground mt-1 line-clamp-2">
|
|
55
60
|
{item.description || 'No description available'}
|
|
56
61
|
</p>
|
|
57
62
|
</div>
|
|
58
63
|
</div>
|
|
59
64
|
|
|
60
|
-
<div className="flex items-center gap-2">
|
|
65
|
+
<div className="flex items-center gap-2 w-full sm:w-auto">
|
|
61
66
|
{type === 'tool' && (
|
|
62
67
|
<button
|
|
63
68
|
onClick={handleUseInChat}
|
|
64
|
-
className="btn btn-primary flex items-center gap-2"
|
|
69
|
+
className="btn btn-primary flex items-center gap-2 flex-1 sm:flex-none text-sm"
|
|
65
70
|
>
|
|
66
71
|
<MessageSquare className="w-4 h-4" />
|
|
67
|
-
Use in Chat
|
|
72
|
+
<span className="hidden sm:inline">Use in Chat</span>
|
|
73
|
+
<span className="sm:hidden">Chat</span>
|
|
68
74
|
</button>
|
|
69
75
|
)}
|
|
70
76
|
<button
|
|
71
77
|
onClick={closeEnlargeModal}
|
|
72
|
-
className="btn btn-ghost w-10 h-10 p-0 flex items-center justify-center"
|
|
78
|
+
className="btn btn-ghost w-10 h-10 p-0 flex items-center justify-center flex-shrink-0"
|
|
73
79
|
aria-label="Close"
|
|
74
80
|
>
|
|
75
81
|
<X className="w-5 h-5" />
|
|
@@ -78,7 +84,7 @@ export function EnlargeModal() {
|
|
|
78
84
|
</div>
|
|
79
85
|
|
|
80
86
|
{/* Widget Content */}
|
|
81
|
-
<div className="h-[calc(100%-
|
|
87
|
+
<div className="h-[calc(100%-120px)] sm:h-[calc(100%-100px)] p-3 sm:p-6 overflow-auto bg-background">
|
|
82
88
|
{componentUri && widgetData ? (
|
|
83
89
|
<div className="w-full h-full rounded-xl overflow-hidden border border-border shadow-inner">
|
|
84
90
|
<WidgetRenderer uri={componentUri} data={widgetData} className="w-full h-full" />
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { Copy, Check, ChevronDown, ChevronRight } from 'lucide-react';
|
|
5
|
+
|
|
6
|
+
interface LogMessageProps {
|
|
7
|
+
message: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function LogMessage({ message }: LogMessageProps) {
|
|
11
|
+
const [copied, setCopied] = useState(false);
|
|
12
|
+
const [isExpanded, setIsExpanded] = useState(true);
|
|
13
|
+
|
|
14
|
+
const handleCopy = async (text: string) => {
|
|
15
|
+
try {
|
|
16
|
+
await navigator.clipboard.writeText(text);
|
|
17
|
+
setCopied(true);
|
|
18
|
+
setTimeout(() => setCopied(false), 2000);
|
|
19
|
+
} catch (error) {
|
|
20
|
+
console.error('Failed to copy:', error);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Try to parse and format JSON
|
|
25
|
+
const tryParseJSON = (text: string): { isJSON: boolean; formatted?: string; parsed?: any } => {
|
|
26
|
+
try {
|
|
27
|
+
const trimmed = text.trim();
|
|
28
|
+
if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) {
|
|
29
|
+
return { isJSON: false };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const parsed = JSON.parse(trimmed);
|
|
33
|
+
|
|
34
|
+
// Pretty print with proper indentation
|
|
35
|
+
const formatted = JSON.stringify(parsed, (key, value) => {
|
|
36
|
+
// If value is a string that looks like JSON, try to parse it
|
|
37
|
+
if (typeof value === 'string' && (value.trim().startsWith('{') || value.trim().startsWith('['))) {
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(value);
|
|
40
|
+
} catch {
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return value;
|
|
45
|
+
}, 2);
|
|
46
|
+
|
|
47
|
+
return { isJSON: true, formatted, parsed };
|
|
48
|
+
} catch {
|
|
49
|
+
return { isJSON: false };
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// Syntax highlight JSON
|
|
54
|
+
const highlightJSON = (jsonString: string) => {
|
|
55
|
+
return jsonString
|
|
56
|
+
.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?)/g, (match) => {
|
|
57
|
+
let cls = 'json-string';
|
|
58
|
+
if (/:$/.test(match)) {
|
|
59
|
+
cls = 'json-key';
|
|
60
|
+
}
|
|
61
|
+
return `<span class="${cls}">${match}</span>`;
|
|
62
|
+
})
|
|
63
|
+
.replace(/\b(true|false|null)\b/g, '<span class="json-boolean">$1</span>')
|
|
64
|
+
.replace(/\b(-?\d+\.?\d*)\b/g, '<span class="json-number">$1</span>')
|
|
65
|
+
.replace(/([{}[\],])/g, '<span class="json-punctuation">$1</span>');
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const result = tryParseJSON(message);
|
|
69
|
+
|
|
70
|
+
if (!result.isJSON) {
|
|
71
|
+
// Plain text message
|
|
72
|
+
return <span className="text-slate-300 break-all">{message}</span>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// JSON message - collapsible with syntax highlighting
|
|
76
|
+
return (
|
|
77
|
+
<div className="flex-1 min-w-0">
|
|
78
|
+
<div className="flex items-center gap-2 mb-1">
|
|
79
|
+
<button
|
|
80
|
+
onClick={() => setIsExpanded(!isExpanded)}
|
|
81
|
+
className="text-slate-400 hover:text-slate-200 transition-colors"
|
|
82
|
+
>
|
|
83
|
+
{isExpanded ? (
|
|
84
|
+
<ChevronDown className="w-4 h-4" />
|
|
85
|
+
) : (
|
|
86
|
+
<ChevronRight className="w-4 h-4" />
|
|
87
|
+
)}
|
|
88
|
+
</button>
|
|
89
|
+
<span className="text-slate-500 text-xs font-semibold">JSON Response</span>
|
|
90
|
+
<button
|
|
91
|
+
onClick={() => handleCopy(result.formatted || message)}
|
|
92
|
+
className="ml-auto text-slate-500 hover:text-slate-300 transition-colors p-1 rounded hover:bg-slate-800"
|
|
93
|
+
title={copied ? 'Copied!' : 'Copy JSON'}
|
|
94
|
+
>
|
|
95
|
+
{copied ? (
|
|
96
|
+
<Check className="w-3.5 h-3.5 text-green-400" />
|
|
97
|
+
) : (
|
|
98
|
+
<Copy className="w-3.5 h-3.5" />
|
|
99
|
+
)}
|
|
100
|
+
</button>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
{isExpanded && (
|
|
104
|
+
<div className="json-container">
|
|
105
|
+
<pre
|
|
106
|
+
className="json-content"
|
|
107
|
+
dangerouslySetInnerHTML={{ __html: highlightJSON(result.formatted || message) }}
|
|
108
|
+
/>
|
|
109
|
+
</div>
|
|
110
|
+
)}
|
|
111
|
+
|
|
112
|
+
<style jsx>{`
|
|
113
|
+
.json-container {
|
|
114
|
+
background: rgb(15, 23, 42);
|
|
115
|
+
border: 1px solid rgb(51, 65, 85);
|
|
116
|
+
border-radius: 0.5rem;
|
|
117
|
+
padding: 0.75rem;
|
|
118
|
+
overflow-x: auto;
|
|
119
|
+
margin-top: 0.5rem;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.json-content {
|
|
123
|
+
margin: 0;
|
|
124
|
+
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
|
125
|
+
font-size: 0.75rem;
|
|
126
|
+
line-height: 1.5;
|
|
127
|
+
color: rgb(226, 232, 240);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
:global(.json-key) {
|
|
131
|
+
color: rgb(96, 165, 250); /* Blue */
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
:global(.json-string) {
|
|
135
|
+
color: rgb(134, 239, 172); /* Green */
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
:global(.json-number) {
|
|
139
|
+
color: rgb(251, 146, 60); /* Orange */
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
:global(.json-boolean) {
|
|
143
|
+
color: rgb(167, 139, 250); /* Purple */
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
:global(.json-punctuation) {
|
|
147
|
+
color: rgb(148, 163, 184); /* Gray */
|
|
148
|
+
}
|
|
149
|
+
`}</style>
|
|
150
|
+
</div>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|