nitrostack 1.0.65 → 1.0.66

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 (55) hide show
  1. package/package.json +2 -1
  2. package/src/studio/README.md +140 -0
  3. package/src/studio/app/api/auth/fetch-metadata/route.ts +71 -0
  4. package/src/studio/app/api/auth/register-client/route.ts +67 -0
  5. package/src/studio/app/api/chat/route.ts +250 -0
  6. package/src/studio/app/api/health/checks/route.ts +42 -0
  7. package/src/studio/app/api/health/route.ts +13 -0
  8. package/src/studio/app/api/init/route.ts +109 -0
  9. package/src/studio/app/api/ping/route.ts +13 -0
  10. package/src/studio/app/api/prompts/[name]/route.ts +21 -0
  11. package/src/studio/app/api/prompts/route.ts +13 -0
  12. package/src/studio/app/api/resources/[...uri]/route.ts +18 -0
  13. package/src/studio/app/api/resources/route.ts +13 -0
  14. package/src/studio/app/api/roots/route.ts +13 -0
  15. package/src/studio/app/api/sampling/route.ts +14 -0
  16. package/src/studio/app/api/tools/[name]/call/route.ts +41 -0
  17. package/src/studio/app/api/tools/route.ts +23 -0
  18. package/src/studio/app/api/widget-examples/route.ts +44 -0
  19. package/src/studio/app/auth/callback/page.tsx +175 -0
  20. package/src/studio/app/auth/page.tsx +560 -0
  21. package/src/studio/app/chat/page.tsx +1133 -0
  22. package/src/studio/app/chat/page.tsx.backup +390 -0
  23. package/src/studio/app/globals.css +486 -0
  24. package/src/studio/app/health/page.tsx +179 -0
  25. package/src/studio/app/layout.tsx +68 -0
  26. package/src/studio/app/logs/page.tsx +279 -0
  27. package/src/studio/app/page.tsx +351 -0
  28. package/src/studio/app/page.tsx.backup +346 -0
  29. package/src/studio/app/ping/page.tsx +209 -0
  30. package/src/studio/app/prompts/page.tsx +230 -0
  31. package/src/studio/app/resources/page.tsx +315 -0
  32. package/src/studio/app/settings/page.tsx +199 -0
  33. package/src/studio/branding.md +807 -0
  34. package/src/studio/components/EnlargeModal.tsx +138 -0
  35. package/src/studio/components/LogMessage.tsx +153 -0
  36. package/src/studio/components/MarkdownRenderer.tsx +410 -0
  37. package/src/studio/components/Sidebar.tsx +295 -0
  38. package/src/studio/components/ToolCard.tsx +139 -0
  39. package/src/studio/components/WidgetRenderer.tsx +346 -0
  40. package/src/studio/lib/api.ts +207 -0
  41. package/src/studio/lib/http-client-transport.ts +222 -0
  42. package/src/studio/lib/llm-service.ts +480 -0
  43. package/src/studio/lib/log-manager.ts +76 -0
  44. package/src/studio/lib/mcp-client.ts +258 -0
  45. package/src/studio/lib/store.ts +192 -0
  46. package/src/studio/lib/theme-provider.tsx +50 -0
  47. package/src/studio/lib/types.ts +107 -0
  48. package/src/studio/lib/widget-loader.ts +90 -0
  49. package/src/studio/middleware.ts +27 -0
  50. package/src/studio/next.config.js +38 -0
  51. package/src/studio/package.json +35 -0
  52. package/src/studio/postcss.config.mjs +10 -0
  53. package/src/studio/public/nitrocloud.png +0 -0
  54. package/src/studio/tailwind.config.ts +67 -0
  55. package/src/studio/tsconfig.json +42 -0
@@ -0,0 +1,138 @@
1
+ 'use client';
2
+
3
+ import { useStudioStore } from '@/lib/store';
4
+ import { WidgetRenderer } from './WidgetRenderer';
5
+ import { X, MessageSquare } from 'lucide-react';
6
+ import { useRouter } from 'next/navigation';
7
+
8
+ export function EnlargeModal() {
9
+ const { enlargeModal, closeEnlargeModal, setCurrentTab } = useStudioStore();
10
+ const router = useRouter();
11
+
12
+ if (!enlargeModal.open || !enlargeModal.item) return null;
13
+
14
+ const { type, item } = enlargeModal;
15
+
16
+ // Get widget URI
17
+ const componentUri =
18
+ type === 'tool'
19
+ ? (item.widget?.route || item.outputTemplate || item._meta?.['openai/outputTemplate'] || item._meta?.['ui/template'])
20
+ : item.uri;
21
+
22
+ // Get data from item's examples or responseData - check both examples and _meta
23
+ const widgetData =
24
+ item.examples?.response ||
25
+ item._meta?.['tool/examples']?.response ||
26
+ item.responseData ||
27
+ {};
28
+
29
+ // Debug logging
30
+ console.log('EnlargeModal - Widget info:', {
31
+ type,
32
+ itemName: item.name,
33
+ componentUri,
34
+ hasWidgetData: !!widgetData && Object.keys(widgetData).length > 0,
35
+ widgetDataType: widgetData ? typeof widgetData : 'none',
36
+ itemExamples: item.examples,
37
+ itemMeta: item._meta,
38
+ metaExamples: item._meta?.['tool/examples'],
39
+ });
40
+
41
+ const handleUseInChat = () => {
42
+ if (type !== 'tool') return;
43
+
44
+ closeEnlargeModal();
45
+
46
+ // Build the tool execution message
47
+ const toolMessage = `Use the ${item.name} tool`;
48
+
49
+ // Store both the tool name and the message
50
+ if (typeof window !== 'undefined') {
51
+ window.localStorage.setItem('suggestedTool', item.name);
52
+ window.localStorage.setItem('chatInput', toolMessage);
53
+ }
54
+
55
+ setCurrentTab('chat');
56
+ router.push('/chat');
57
+ };
58
+
59
+ return (
60
+ <div
61
+ className="fixed inset-0 z-50 flex items-center justify-center animate-fade-in"
62
+ style={{ backgroundColor: 'rgba(0, 0, 0, 0.85)' }}
63
+ onClick={closeEnlargeModal}
64
+ >
65
+ <div
66
+ 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"
67
+ onClick={(e) => e.stopPropagation()}
68
+ >
69
+ {/* Header */}
70
+ <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">
71
+ <div className="flex items-start sm:items-center gap-3 sm:gap-4 flex-1 min-w-0">
72
+ <span className="text-2xl sm:text-3xl flex-shrink-0">{type === 'tool' ? '⚡' : '🎨'}</span>
73
+ <div className="min-w-0 flex-1">
74
+ <h2 className="text-lg sm:text-xl font-bold text-foreground truncate">{item.name}</h2>
75
+ <p className="text-xs sm:text-sm text-muted-foreground mt-1 line-clamp-2">
76
+ {item.description || 'No description available'}
77
+ </p>
78
+ </div>
79
+ </div>
80
+
81
+ <div className="flex items-center gap-2 w-full sm:w-auto">
82
+ {type === 'tool' && (
83
+ <button
84
+ onClick={handleUseInChat}
85
+ className="btn btn-primary flex items-center gap-2 flex-1 sm:flex-none text-sm"
86
+ >
87
+ <MessageSquare className="w-4 h-4" />
88
+ <span className="hidden sm:inline">Use in Chat</span>
89
+ <span className="sm:hidden">Chat</span>
90
+ </button>
91
+ )}
92
+ <button
93
+ onClick={closeEnlargeModal}
94
+ className="btn btn-ghost w-10 h-10 p-0 flex items-center justify-center flex-shrink-0"
95
+ aria-label="Close"
96
+ >
97
+ <X className="w-5 h-5" />
98
+ </button>
99
+ </div>
100
+ </div>
101
+
102
+ {/* Widget Content */}
103
+ <div className="h-[calc(100%-120px)] sm:h-[calc(100%-100px)] p-3 sm:p-6 overflow-auto bg-background">
104
+ {componentUri && widgetData ? (
105
+ <div className="w-full h-full rounded-xl overflow-hidden border border-border shadow-inner">
106
+ <WidgetRenderer uri={componentUri} data={widgetData} className="w-full h-full" />
107
+ </div>
108
+ ) : (
109
+ <div className="flex items-center justify-center h-full">
110
+ <div className="text-center">
111
+ <p className="text-muted-foreground mb-2">
112
+ {!componentUri ? 'No widget URI available' : 'No example data available'}
113
+ </p>
114
+ <p className="text-xs text-muted-foreground">
115
+ {type === 'tool' ? 'This tool does not have a UI widget attached' : 'No preview data found'}
116
+ </p>
117
+ </div>
118
+ </div>
119
+ )}
120
+ </div>
121
+
122
+ {/* Metadata Footer */}
123
+ {type === 'tool' && item.inputSchema && (
124
+ <div className="absolute bottom-0 left-0 right-0 p-4 border-t border-border glass">
125
+ <details className="text-sm">
126
+ <summary className="cursor-pointer text-muted-foreground hover:text-foreground font-medium">
127
+ Input Schema
128
+ </summary>
129
+ <pre className="mt-2 p-3 bg-muted rounded-lg text-xs text-foreground overflow-auto max-h-40 font-mono">
130
+ {JSON.stringify(item.inputSchema, null, 2)}
131
+ </pre>
132
+ </details>
133
+ </div>
134
+ )}
135
+ </div>
136
+ </div>
137
+ );
138
+ }
@@ -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
+
@@ -0,0 +1,410 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { Copy, Check } from 'lucide-react';
5
+
6
+ interface MarkdownRendererProps {
7
+ content: string;
8
+ }
9
+
10
+ export function MarkdownRenderer({ content }: MarkdownRendererProps) {
11
+ const [copied, setCopied] = useState(false);
12
+ const [copiedCode, setCopiedCode] = useState<string | null>(null);
13
+
14
+ const handleCopy = async () => {
15
+ try {
16
+ await navigator.clipboard.writeText(content);
17
+ setCopied(true);
18
+ setTimeout(() => setCopied(false), 2000);
19
+ } catch (error) {
20
+ console.error('Failed to copy:', error);
21
+ }
22
+ };
23
+
24
+ const handleCopyCode = async (code: string, blockId: string) => {
25
+ try {
26
+ await navigator.clipboard.writeText(code);
27
+ setCopiedCode(blockId);
28
+ setTimeout(() => setCopiedCode(null), 2000);
29
+ } catch (error) {
30
+ console.error('Failed to copy code:', error);
31
+ }
32
+ };
33
+
34
+ // Simple syntax highlighting for common languages
35
+ const highlightCode = (code: string, language: string) => {
36
+ let highlighted = escapeHtml(code);
37
+
38
+ // Keywords for different languages
39
+ const keywords: Record<string, string[]> = {
40
+ javascript: ['function', 'const', 'let', 'var', 'return', 'if', 'else', 'for', 'while', 'class', 'import', 'export', 'from', 'async', 'await', 'new', 'this', 'try', 'catch'],
41
+ typescript: ['function', 'const', 'let', 'var', 'return', 'if', 'else', 'for', 'while', 'class', 'import', 'export', 'from', 'async', 'await', 'interface', 'type', 'enum', 'new', 'this', 'try', 'catch'],
42
+ python: ['def', 'class', 'return', 'if', 'else', 'elif', 'for', 'while', 'import', 'from', 'try', 'except', 'with', 'as', 'pass', 'break', 'continue', 'lambda', 'yield'],
43
+ java: ['public', 'private', 'protected', 'class', 'interface', 'void', 'return', 'if', 'else', 'for', 'while', 'new', 'this', 'static', 'final', 'try', 'catch'],
44
+ go: ['func', 'var', 'const', 'return', 'if', 'else', 'for', 'range', 'struct', 'interface', 'type', 'package', 'import', 'go', 'defer', 'chan'],
45
+ };
46
+
47
+ const lang = language.toLowerCase();
48
+ const langKeywords = keywords[lang] || keywords['javascript'];
49
+
50
+ // Comments
51
+ highlighted = highlighted.replace(/(\/\/.*$|\/\*[\s\S]*?\*\/)/gm, '<span class="syntax-comment">$1</span>');
52
+ highlighted = highlighted.replace(/(#.*$)/gm, '<span class="syntax-comment">$1</span>');
53
+
54
+ // Strings
55
+ highlighted = highlighted.replace(/("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)/g, '<span class="syntax-string">$1</span>');
56
+
57
+ // Numbers
58
+ highlighted = highlighted.replace(/\b(\d+\.?\d*)\b/g, '<span class="syntax-number">$1</span>');
59
+
60
+ // Keywords
61
+ langKeywords.forEach(keyword => {
62
+ const regex = new RegExp(`\\b${keyword}\\b`, 'g');
63
+ highlighted = highlighted.replace(regex, `<span class="syntax-keyword">${keyword}</span>`);
64
+ });
65
+
66
+ // Function calls
67
+ highlighted = highlighted.replace(/\b([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/g, '<span class="syntax-function">$1</span>(');
68
+
69
+ return highlighted;
70
+ };
71
+
72
+ // Simple markdown rendering using regex (basic implementation)
73
+ const renderMarkdown = (text: string) => {
74
+ let html = text;
75
+ let codeBlockCounter = 0;
76
+
77
+ // Code blocks (```language\ncode\n```)
78
+ html = html.replace(/```(\w+)?\n([\s\S]*?)```/g, (_, lang, code) => {
79
+ const blockId = `code-block-${codeBlockCounter++}`;
80
+ const language = lang || 'text';
81
+ const trimmedCode = code.trim();
82
+ const highlightedCode = language !== 'text' ? highlightCode(trimmedCode, language) : escapeHtml(trimmedCode);
83
+
84
+ return `<div class="code-block-wrapper">
85
+ <div class="code-block-header">
86
+ <span class="code-language">${language}</span>
87
+ <button class="copy-code-btn" data-code="${escapeHtml(trimmedCode)}" data-block-id="${blockId}" onclick="window.copyCodeBlock('${blockId}', \`${escapeHtml(trimmedCode).replace(/`/g, '\\`')}\`)">
88
+ <svg class="copy-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
89
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
90
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
91
+ </svg>
92
+ <span class="copy-text">Copy code</span>
93
+ </button>
94
+ </div>
95
+ <pre class="code-block"><code class="language-${language}">${highlightedCode}</code></pre>
96
+ </div>`;
97
+ });
98
+
99
+ // Inline code (`code`)
100
+ html = html.replace(/`([^`]+)`/g, '<code class="inline-code">$1</code>');
101
+
102
+ // Bold (**text** or __text__)
103
+ html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
104
+ html = html.replace(/__(.+?)__/g, '<strong>$1</strong>');
105
+
106
+ // Italic (*text* or _text_)
107
+ html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
108
+ html = html.replace(/_(.+?)_/g, '<em>$1</em>');
109
+
110
+ // Headers (# text)
111
+ html = html.replace(/^### (.+)$/gm, '<h3 class="md-h3">$1</h3>');
112
+ html = html.replace(/^## (.+)$/gm, '<h2 class="md-h2">$1</h2>');
113
+ html = html.replace(/^# (.+)$/gm, '<h1 class="md-h1">$1</h1>');
114
+
115
+ // Links ([text](url))
116
+ html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" class="md-link" target="_blank" rel="noopener noreferrer">$1</a>');
117
+
118
+ // Unordered lists (- item or * item)
119
+ html = html.replace(/^[\*\-] (.+)$/gm, '<li class="md-li">$1</li>');
120
+ html = html.replace(/(<li class="md-li">.*<\/li>\n?)+/g, '<ul class="md-ul">$&</ul>');
121
+
122
+ // Ordered lists (1. item)
123
+ html = html.replace(/^\d+\. (.+)$/gm, '<li class="md-li">$1</li>');
124
+
125
+ // Blockquotes (> text)
126
+ html = html.replace(/^> (.+)$/gm, '<blockquote class="md-blockquote">$1</blockquote>');
127
+
128
+ // Horizontal rule (---)
129
+ html = html.replace(/^---$/gm, '<hr class="md-hr" />');
130
+
131
+ // Line breaks
132
+ html = html.replace(/\n/g, '<br />');
133
+
134
+ return html;
135
+ };
136
+
137
+ const escapeHtml = (text: string) => {
138
+ const div = document.createElement('div');
139
+ div.textContent = text;
140
+ return div.innerHTML;
141
+ };
142
+
143
+ // Check if content has markdown syntax
144
+ const hasMarkdown = (text: string) => {
145
+ const markdownPatterns = [
146
+ /```[\s\S]*?```/, // Code blocks
147
+ /`[^`]+`/, // Inline code
148
+ /\*\*[^*]+\*\*/, // Bold
149
+ /__[^_]+__/, // Bold
150
+ /\*[^*]+\*/, // Italic
151
+ /_[^_]+_/, // Italic
152
+ /^#{1,6} .+$/m, // Headers
153
+ /\[.+\]\(.+\)/, // Links
154
+ /^[\*\-] .+$/m, // Unordered lists
155
+ /^\d+\. .+$/m, // Ordered lists
156
+ /^> .+$/m, // Blockquotes
157
+ ];
158
+
159
+ return markdownPatterns.some(pattern => pattern.test(text));
160
+ };
161
+
162
+ const isMarkdown = hasMarkdown(content);
163
+
164
+ if (!isMarkdown) {
165
+ // No markdown detected, render as plain text
166
+ return <div className="whitespace-pre-wrap">{content}</div>;
167
+ }
168
+
169
+ // Set up global copy function for code blocks
170
+ if (typeof window !== 'undefined') {
171
+ (window as any).copyCodeBlock = async (blockId: string, code: string) => {
172
+ try {
173
+ await navigator.clipboard.writeText(code);
174
+ setCopiedCode(blockId);
175
+ setTimeout(() => setCopiedCode(null), 2000);
176
+ } catch (error) {
177
+ console.error('Failed to copy code:', error);
178
+ }
179
+ };
180
+ }
181
+
182
+ return (
183
+ <div className="relative markdown-container">
184
+ {/* Copy All Button */}
185
+ <button
186
+ onClick={handleCopy}
187
+ className="absolute top-3 right-3 z-10 px-3 py-1.5 rounded-lg bg-slate-700/80 hover:bg-slate-700 border border-slate-600/50 transition-all text-xs font-medium flex items-center gap-2"
188
+ title={copied ? 'Copied!' : 'Copy all content'}
189
+ >
190
+ {copied ? (
191
+ <>
192
+ <Check className="w-3.5 h-3.5 text-green-400" />
193
+ <span className="text-green-400">Copied!</span>
194
+ </>
195
+ ) : (
196
+ <>
197
+ <Copy className="w-3.5 h-3.5 text-slate-300" />
198
+ <span className="text-slate-300">Copy all</span>
199
+ </>
200
+ )}
201
+ </button>
202
+
203
+ {/* Markdown Content */}
204
+ <div
205
+ className="markdown-content"
206
+ dangerouslySetInnerHTML={{ __html: renderMarkdown(content) }}
207
+ />
208
+
209
+ <style jsx>{`
210
+ .markdown-container {
211
+ position: relative;
212
+ padding: 1.25rem;
213
+ background: linear-gradient(to bottom, rgb(30, 41, 59), rgb(15, 23, 42));
214
+ border: 1px solid rgb(51, 65, 85);
215
+ border-radius: 0.75rem;
216
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.2);
217
+ }
218
+
219
+ :global(.markdown-content) {
220
+ font-size: 0.875rem;
221
+ line-height: 1.625;
222
+ color: rgb(226, 232, 240);
223
+ }
224
+
225
+ :global(.code-block-wrapper) {
226
+ position: relative;
227
+ margin: 1.5rem 0;
228
+ border-radius: 0.5rem;
229
+ overflow: hidden;
230
+ background: rgb(15, 23, 42);
231
+ border: 1px solid rgb(51, 65, 85);
232
+ }
233
+
234
+ :global(.code-block-header) {
235
+ display: flex;
236
+ align-items: center;
237
+ justify-content: space-between;
238
+ padding: 0.5rem 1rem;
239
+ background: rgb(30, 41, 59);
240
+ border-bottom: 1px solid rgb(51, 65, 85);
241
+ }
242
+
243
+ :global(.code-language) {
244
+ font-size: 0.75rem;
245
+ font-weight: 500;
246
+ color: rgb(148, 163, 184);
247
+ text-transform: lowercase;
248
+ }
249
+
250
+ :global(.copy-code-btn) {
251
+ display: flex;
252
+ align-items: center;
253
+ gap: 0.375rem;
254
+ padding: 0.25rem 0.625rem;
255
+ font-size: 0.75rem;
256
+ color: rgb(203, 213, 225);
257
+ background: transparent;
258
+ border: none;
259
+ border-radius: 0.375rem;
260
+ cursor: pointer;
261
+ transition: all 0.2s;
262
+ }
263
+
264
+ :global(.copy-code-btn:hover) {
265
+ background: rgb(51, 65, 85);
266
+ color: rgb(226, 232, 240);
267
+ }
268
+
269
+ :global(.copy-icon) {
270
+ width: 14px;
271
+ height: 14px;
272
+ }
273
+
274
+ :global(.markdown-content .md-h1) {
275
+ font-size: 1.5rem;
276
+ font-weight: 700;
277
+ margin-top: 1.5rem;
278
+ margin-bottom: 0.75rem;
279
+ color: rgb(241, 245, 249);
280
+ border-bottom: 1px solid rgb(51, 65, 85);
281
+ padding-bottom: 0.5rem;
282
+ }
283
+
284
+ :global(.markdown-content .md-h2) {
285
+ font-size: 1.25rem;
286
+ font-weight: 700;
287
+ margin-top: 1.25rem;
288
+ margin-bottom: 0.5rem;
289
+ color: rgb(226, 232, 240);
290
+ }
291
+
292
+ :global(.markdown-content .md-h3) {
293
+ font-size: 1.125rem;
294
+ font-weight: 600;
295
+ margin-top: 1rem;
296
+ margin-bottom: 0.5rem;
297
+ color: rgb(203, 213, 225);
298
+ }
299
+
300
+ :global(.markdown-content .code-block) {
301
+ background: rgb(15, 23, 42);
302
+ padding: 1rem;
303
+ margin: 0;
304
+ overflow-x: auto;
305
+ font-family: 'JetBrains Mono', 'Courier New', monospace;
306
+ font-size: 0.8125rem;
307
+ line-height: 1.6;
308
+ }
309
+
310
+ :global(.markdown-content .code-block code) {
311
+ background: transparent;
312
+ padding: 0;
313
+ border: none;
314
+ color: rgb(226, 232, 240);
315
+ }
316
+
317
+ /* Syntax highlighting colors */
318
+ :global(.syntax-keyword) {
319
+ color: rgb(96, 165, 250); /* Blue */
320
+ font-weight: 500;
321
+ }
322
+
323
+ :global(.syntax-string) {
324
+ color: rgb(134, 239, 172); /* Green */
325
+ }
326
+
327
+ :global(.syntax-number) {
328
+ color: rgb(251, 146, 60); /* Orange */
329
+ }
330
+
331
+ :global(.syntax-comment) {
332
+ color: rgb(100, 116, 139); /* Gray */
333
+ font-style: italic;
334
+ }
335
+
336
+ :global(.syntax-function) {
337
+ color: rgb(253, 224, 71); /* Yellow */
338
+ }
339
+
340
+ :global(.markdown-content .inline-code) {
341
+ background: rgb(30, 41, 59);
342
+ color: rgb(251, 146, 60);
343
+ padding: 0.125rem 0.5rem;
344
+ border-radius: 0.25rem;
345
+ font-family: 'JetBrains Mono', 'Courier New', monospace;
346
+ font-size: 0.875em;
347
+ border: 1px solid rgb(51, 65, 85);
348
+ }
349
+
350
+ :global(.markdown-content strong) {
351
+ font-weight: 600;
352
+ color: rgb(241, 245, 249);
353
+ }
354
+
355
+ :global(.markdown-content em) {
356
+ font-style: italic;
357
+ color: rgb(203, 213, 225);
358
+ }
359
+
360
+ :global(.markdown-content .md-link) {
361
+ color: rgb(96, 165, 250);
362
+ text-decoration: underline;
363
+ text-decoration-color: transparent;
364
+ transition: text-decoration-color 0.2s;
365
+ }
366
+
367
+ :global(.markdown-content .md-link:hover) {
368
+ text-decoration-color: rgb(96, 165, 250);
369
+ }
370
+
371
+ :global(.markdown-content .md-ul) {
372
+ list-style-type: disc;
373
+ margin-left: 1.5rem;
374
+ margin-top: 0.5rem;
375
+ margin-bottom: 0.5rem;
376
+ }
377
+
378
+ :global(.markdown-content .md-li) {
379
+ margin-top: 0.25rem;
380
+ margin-bottom: 0.25rem;
381
+ }
382
+
383
+ :global(.markdown-content .md-blockquote) {
384
+ border-left: 3px solid rgb(96, 165, 250);
385
+ padding-left: 1rem;
386
+ padding-top: 0.5rem;
387
+ padding-bottom: 0.5rem;
388
+ margin: 1rem 0;
389
+ background: rgb(30, 41, 59);
390
+ color: rgb(148, 163, 184);
391
+ font-style: italic;
392
+ border-radius: 0 0.25rem 0.25rem 0;
393
+ }
394
+
395
+ :global(.markdown-content .md-hr) {
396
+ border: none;
397
+ border-top: 1px solid rgb(51, 65, 85);
398
+ margin: 1.5rem 0;
399
+ }
400
+
401
+ :global(.markdown-content br) {
402
+ display: block;
403
+ content: "";
404
+ margin-top: 0.5rem;
405
+ }
406
+ `}</style>
407
+ </div>
408
+ );
409
+ }
410
+