Ravindra commited on
Commit
d25ed8c
·
verified ·
1 Parent(s): 0d0807c

Upload 7 files

Browse files
web/components/AgentPulse.tsx CHANGED
@@ -1,119 +1,119 @@
1
- "use client";
2
-
3
  import { useEffect, useRef } from "react";
4
  import { motion, AnimatePresence } from "framer-motion";
5
  import { Terminal, CheckCircle2, CircleDashed, X } from "lucide-react";
6
  import { cn } from "@/lib/utils";
7
-
8
- interface AgentPulseProps {
9
- logs: string[];
10
- status: 'IDLE' | 'PENDING' | 'STARTED' | 'SUCCESS' | 'FAILURE';
11
- onClose?: () => void;
12
- }
13
-
14
- export function AgentPulse({ logs, status, onClose }: AgentPulseProps) {
15
- const scrollRef = useRef<HTMLDivElement>(null);
16
-
17
- // Auto-scroll to bottom of logs
18
- useEffect(() => {
19
- if (scrollRef.current) {
20
- scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
21
- }
22
- }, [logs]);
23
-
24
- if (status === 'IDLE') return null;
25
-
26
- return (
27
- <motion.div
28
- initial={{ opacity: 0, y: 20 }}
29
- animate={{ opacity: 1, y: 0 }}
30
- className="fixed bottom-6 right-6 w-96 bg-[#0F1117] border border-white/10 rounded-xl shadow-2xl overflow-hidden z-50 font-mono text-xs"
31
- >
32
- {/* Header */}
33
- <div className="flex items-center justify-between px-4 py-2 bg-white/5 border-b border-white/5">
34
- <div className="flex items-center gap-2 text-gray-400">
35
- <Terminal className="w-3.5 h-3.5" />
36
- <span>Agent Neural Link</span>
37
- </div>
38
- <div className="flex items-center gap-2">
39
- {status === 'STARTED' && (
40
- <span className="relative flex h-2 w-2">
41
- <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
42
- <span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-500"></span>
43
- </span>
44
- )}
45
- <span className={cn(
46
- "capitalize mr-2",
47
- status === 'SUCCESS' ? "text-emerald-500" : "text-gray-400"
48
- )}>{status.toLowerCase()}</span>
49
-
50
- {onClose && (
51
- <button
52
- onClick={onClose}
53
- className="text-gray-500 hover:text-white transition-colors"
54
- >
55
- <X className="w-4 h-4" />
56
- </button>
57
- )}
58
- </div>
59
- </div>
60
-
61
- {/* Logs Area */}
62
- <div
63
- ref={scrollRef}
64
- className="h-48 overflow-y-auto p-4 space-y-2 text-gray-300"
65
- >
66
- <AnimatePresence initial={false}>
67
- {logs.map((log, i) => (
68
- <motion.div
69
- key={i}
70
- initial={{ opacity: 0, x: -10 }}
71
- animate={{ opacity: 1, x: 0 }}
72
- className="flex gap-2"
73
- >
74
- <span className="text-emerald-500/50">➜</span>
75
- <span>{log}</span>
76
- </motion.div>
77
- ))}
78
- {status === 'STARTED' && (
79
- <motion.div
80
- initial={{ opacity: 0 }}
81
- animate={{ opacity: 1 }}
82
- transition={{ repeat: Infinity, duration: 1 }}
83
- className="flex gap-2 text-emerald-500/80"
84
- >
85
- <span className="text-emerald-500/50">➜</span>
86
- <span className="animate-pulse">_</span>
87
- </motion.div>
88
- )}
89
- </AnimatePresence>
90
- </div>
91
-
92
- {/* Progress Steps (Visual Mockup) */}
93
- <div className="flex items-center justify-between px-6 py-3 bg-black/20 border-t border-white/5">
94
- <StepIcon label="Research" active={logs.some(l => l.toLowerCase().includes('research'))} />
95
- <div className="h-[1px] flex-1 bg-white/10 mx-2" />
96
- <StepIcon label="Drafting" active={logs.some(l => l.toLowerCase().includes('draft')) || logs.some(l => l.toLowerCase().includes('writ'))} />
97
- <div className="h-[1px] flex-1 bg-white/10 mx-2" />
98
- <StepIcon label="Review" active={status === 'SUCCESS'} />
99
- </div>
100
- </motion.div>
101
- );
102
- }
103
-
104
- function StepIcon({ label, active }: { label: string, active: boolean }) {
105
- return (
106
- <div className="flex flex-col items-center gap-1">
107
- {active ? (
108
- <div className="w-5 h-5 rounded-full bg-emerald-500/20 text-emerald-500 flex items-center justify-center border border-emerald-500/50">
109
- <CheckCircle2 className="w-3 h-3" />
110
- </div>
111
- ) : (
112
- <div className="w-5 h-5 rounded-full bg-white/5 text-gray-600 flex items-center justify-center border border-white/10">
113
- <CircleDashed className="w-3 h-3" />
114
- </div>
115
- )}
116
- <span className={cn("text-[10px]", active ? "text-emerald-500" : "text-gray-600")}>{label}</span>
117
- </div>
118
- )
119
- }
 
1
+ "use client";
2
+
3
  import { useEffect, useRef } from "react";
4
  import { motion, AnimatePresence } from "framer-motion";
5
  import { Terminal, CheckCircle2, CircleDashed, X } from "lucide-react";
6
  import { cn } from "@/lib/utils";
7
+
8
+ interface AgentPulseProps {
9
+ logs: string[];
10
+ status: 'IDLE' | 'PENDING' | 'STARTED' | 'SUCCESS' | 'FAILURE';
11
+ onClose?: () => void;
12
+ }
13
+
14
+ export function AgentPulse({ logs, status, onClose }: AgentPulseProps) {
15
+ const scrollRef = useRef<HTMLDivElement>(null);
16
+
17
+ // Auto-scroll to bottom of logs
18
+ useEffect(() => {
19
+ if (scrollRef.current) {
20
+ scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
21
+ }
22
+ }, [logs]);
23
+
24
+ if (status === 'IDLE') return null;
25
+
26
+ return (
27
+ <motion.div
28
+ initial={{ opacity: 0, y: 20 }}
29
+ animate={{ opacity: 1, y: 0 }}
30
+ className="fixed bottom-6 right-6 w-96 bg-[#0F1117] border border-white/10 rounded-xl shadow-2xl overflow-hidden z-50 font-mono text-xs"
31
+ >
32
+ {/* Header */}
33
+ <div className="flex items-center justify-between px-4 py-2 bg-white/5 border-b border-white/5">
34
+ <div className="flex items-center gap-2 text-gray-400">
35
+ <Terminal className="w-3.5 h-3.5" />
36
+ <span>Agent Neural Link</span>
37
+ </div>
38
+ <div className="flex items-center gap-2">
39
+ {status === 'STARTED' && (
40
+ <span className="relative flex h-2 w-2">
41
+ <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
42
+ <span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-500"></span>
43
+ </span>
44
+ )}
45
+ <span className={cn(
46
+ "capitalize mr-2",
47
+ status === 'SUCCESS' ? "text-emerald-500" : "text-gray-400"
48
+ )}>{status.toLowerCase()}</span>
49
+
50
+ {onClose && (
51
+ <button
52
+ onClick={onClose}
53
+ className="text-gray-500 hover:text-white transition-colors"
54
+ >
55
+ <X className="w-4 h-4" />
56
+ </button>
57
+ )}
58
+ </div>
59
+ </div>
60
+
61
+ {/* Logs Area */}
62
+ <div
63
+ ref={scrollRef}
64
+ className="h-48 overflow-y-auto p-4 space-y-2 text-gray-300"
65
+ >
66
+ <AnimatePresence initial={false}>
67
+ {logs.map((log, i) => (
68
+ <motion.div
69
+ key={i}
70
+ initial={{ opacity: 0, x: -10 }}
71
+ animate={{ opacity: 1, x: 0 }}
72
+ className="flex gap-2"
73
+ >
74
+ <span className="text-emerald-500/50">➜</span>
75
+ <span>{log}</span>
76
+ </motion.div>
77
+ ))}
78
+ {status === 'STARTED' && (
79
+ <motion.div
80
+ initial={{ opacity: 0 }}
81
+ animate={{ opacity: 1 }}
82
+ transition={{ repeat: Infinity, duration: 1 }}
83
+ className="flex gap-2 text-emerald-500/80"
84
+ >
85
+ <span className="text-emerald-500/50">➜</span>
86
+ <span className="animate-pulse">_</span>
87
+ </motion.div>
88
+ )}
89
+ </AnimatePresence>
90
+ </div>
91
+
92
+ {/* Progress Steps (Visual Mockup) */}
93
+ <div className="flex items-center justify-between px-6 py-3 bg-black/20 border-t border-white/5">
94
+ <StepIcon label="Research" active={logs.some(l => l.toLowerCase().includes('research'))} />
95
+ <div className="h-[1px] flex-1 bg-white/10 mx-2" />
96
+ <StepIcon label="Drafting" active={logs.some(l => l.toLowerCase().includes('draft')) || logs.some(l => l.toLowerCase().includes('writ'))} />
97
+ <div className="h-[1px] flex-1 bg-white/10 mx-2" />
98
+ <StepIcon label="Review" active={status === 'SUCCESS'} />
99
+ </div>
100
+ </motion.div>
101
+ );
102
+ }
103
+
104
+ function StepIcon({ label, active }: { label: string, active: boolean }) {
105
+ return (
106
+ <div className="flex flex-col items-center gap-1">
107
+ {active ? (
108
+ <div className="w-5 h-5 rounded-full bg-emerald-500/20 text-emerald-500 flex items-center justify-center border border-emerald-500/50">
109
+ <CheckCircle2 className="w-3 h-3" />
110
+ </div>
111
+ ) : (
112
+ <div className="w-5 h-5 rounded-full bg-white/5 text-gray-600 flex items-center justify-center border border-white/10">
113
+ <CircleDashed className="w-3 h-3" />
114
+ </div>
115
+ )}
116
+ <span className={cn("text-[10px]", active ? "text-emerald-500" : "text-gray-600")}>{label}</span>
117
+ </div>
118
+ )
119
+ }
web/components/ConfigPanel.tsx CHANGED
@@ -1,14 +1,14 @@
1
- "use client";
2
-
3
  import { DomainKey, DOMAINS } from "@/lib/domains";
4
  import { cn } from "@/lib/utils";
5
  import { Zap } from "lucide-react";
6
-
7
  interface ConfigPanelProps {
8
  domain: DomainKey;
9
  topic: string;
10
  setTopic: (t: string) => void;
11
- tone: string;
12
  setTone: (t: string) => void;
13
  onGenerate: () => void;
14
  isGenerating: boolean;
@@ -17,12 +17,12 @@ interface ConfigPanelProps {
17
  resetInSeconds?: number;
18
  mode?: "fingerprint" | "ip_fallback" | "authenticated";
19
  }
20
-
21
- export function ConfigPanel({
22
- domain,
23
- topic,
24
- setTopic,
25
- tone,
26
  setTone,
27
  onGenerate,
28
  isGenerating,
@@ -40,9 +40,9 @@ export function ConfigPanel({
40
  return (
41
  <div className="w-full max-w-3xl mx-auto space-y-6">
42
  <div className="space-y-2">
43
- <h2 className="text-2xl font-semibold text-white">
44
- What should the <span className={activeDomain.accent}>{activeDomain.label}</span> Agents build today?
45
- </h2>
46
  <p className="text-gray-400 text-sm">
47
  Describe your topic in detail. The swarms will handle the rest.
48
  </p>
@@ -62,66 +62,66 @@ export function ConfigPanel({
62
  </p>
63
  )}
64
  </div>
65
-
66
- <div className="group relative">
67
- <div className={cn(
68
- "absolute -inset-0.5 rounded-xl blur opacity-30 transition duration-500",
69
- activeDomain.bg
70
- )}></div>
71
- <div className="relative bg-zinc-900 rounded-xl p-4 border border-white/10 shadow-2xl">
72
- <textarea
73
- value={topic}
74
- onChange={(e) => setTopic(e.target.value)}
75
- placeholder={activeDomain.placeholder}
76
- className="w-full bg-transparent text-lg text-white placeholder-gray-600 focus:outline-none min-h-[120px] resize-none"
77
- />
78
-
79
- <div className="flex items-center justify-between mt-4 pt-4 border-t border-white/5">
80
- <div className="flex items-center gap-4">
81
- <label className="text-sm text-gray-500">Tone:</label>
82
- <div className="flex bg-black/40 rounded-lg p-1">
83
- {['Academic', 'Professional', 'Engaging'].map((t) => (
84
- <button
85
- key={t}
86
- onClick={() => setTone(t)}
87
- className={cn(
88
- "px-3 py-1 text-xs rounded-md transition-all",
89
- tone === t
90
- ? "bg-white/10 text-white shadow-sm"
91
- : "text-gray-500 hover:text-gray-300"
92
- )}
93
- >
94
- {t}
95
- </button>
96
- ))}
97
- </div>
98
- </div>
99
-
100
  <button
101
  onClick={onGenerate}
102
  disabled={disableGenerate}
103
  className={cn(
104
  "flex items-center gap-2 px-6 py-2 rounded-lg font-medium transition-all shadow-lg hover:bg-opacity-90 active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed",
105
  activeDomain.bg,
106
- activeDomain.accent,
107
- "bg-opacity-20 hover:bg-opacity-30 border border-white/10"
108
- )}
109
- >
110
- {isGenerating ? (
111
- <>
112
- <div className="w-4 h-4 border-2 border-current border-t-transparent rounded-full animate-spin" />
113
- Agents Working...
114
- </>
115
- ) : (
116
- <>
117
- <Zap className="w-4 h-4 fill-current" />
118
- Ignite Swarm
119
- </>
120
- )}
121
- </button>
122
- </div>
123
- </div>
124
- </div>
125
- </div>
126
- );
127
- }
 
1
+ "use client";
2
+
3
  import { DomainKey, DOMAINS } from "@/lib/domains";
4
  import { cn } from "@/lib/utils";
5
  import { Zap } from "lucide-react";
6
+
7
  interface ConfigPanelProps {
8
  domain: DomainKey;
9
  topic: string;
10
  setTopic: (t: string) => void;
11
+ tone: string;
12
  setTone: (t: string) => void;
13
  onGenerate: () => void;
14
  isGenerating: boolean;
 
17
  resetInSeconds?: number;
18
  mode?: "fingerprint" | "ip_fallback" | "authenticated";
19
  }
20
+
21
+ export function ConfigPanel({
22
+ domain,
23
+ topic,
24
+ setTopic,
25
+ tone,
26
  setTone,
27
  onGenerate,
28
  isGenerating,
 
40
  return (
41
  <div className="w-full max-w-3xl mx-auto space-y-6">
42
  <div className="space-y-2">
43
+ <h2 className="text-2xl font-semibold text-white">
44
+ What should the <span className={activeDomain.accent}>{activeDomain.label}</span> Agents build today?
45
+ </h2>
46
  <p className="text-gray-400 text-sm">
47
  Describe your topic in detail. The swarms will handle the rest.
48
  </p>
 
62
  </p>
63
  )}
64
  </div>
65
+
66
+ <div className="group relative">
67
+ <div className={cn(
68
+ "absolute -inset-0.5 rounded-xl blur opacity-30 transition duration-500",
69
+ activeDomain.bg
70
+ )}></div>
71
+ <div className="relative bg-zinc-900 rounded-xl p-4 border border-white/10 shadow-2xl">
72
+ <textarea
73
+ value={topic}
74
+ onChange={(e) => setTopic(e.target.value)}
75
+ placeholder={activeDomain.placeholder}
76
+ className="w-full bg-transparent text-lg text-white placeholder-gray-600 focus:outline-none min-h-[120px] resize-none"
77
+ />
78
+
79
+ <div className="flex items-center justify-between mt-4 pt-4 border-t border-white/5">
80
+ <div className="flex items-center gap-4">
81
+ <label className="text-sm text-gray-500">Tone:</label>
82
+ <div className="flex bg-black/40 rounded-lg p-1">
83
+ {['Academic', 'Professional', 'Engaging'].map((t) => (
84
+ <button
85
+ key={t}
86
+ onClick={() => setTone(t)}
87
+ className={cn(
88
+ "px-3 py-1 text-xs rounded-md transition-all",
89
+ tone === t
90
+ ? "bg-white/10 text-white shadow-sm"
91
+ : "text-gray-500 hover:text-gray-300"
92
+ )}
93
+ >
94
+ {t}
95
+ </button>
96
+ ))}
97
+ </div>
98
+ </div>
99
+
100
  <button
101
  onClick={onGenerate}
102
  disabled={disableGenerate}
103
  className={cn(
104
  "flex items-center gap-2 px-6 py-2 rounded-lg font-medium transition-all shadow-lg hover:bg-opacity-90 active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed",
105
  activeDomain.bg,
106
+ activeDomain.accent,
107
+ "bg-opacity-20 hover:bg-opacity-30 border border-white/10"
108
+ )}
109
+ >
110
+ {isGenerating ? (
111
+ <>
112
+ <div className="w-4 h-4 border-2 border-current border-t-transparent rounded-full animate-spin" />
113
+ Agents Working...
114
+ </>
115
+ ) : (
116
+ <>
117
+ <Zap className="w-4 h-4 fill-current" />
118
+ Ignite Swarm
119
+ </>
120
+ )}
121
+ </button>
122
+ </div>
123
+ </div>
124
+ </div>
125
+ </div>
126
+ );
127
+ }
web/components/ContentCanvas.tsx CHANGED
@@ -1,267 +1,267 @@
1
- "use client";
2
-
3
  import MarkdownPreview from '@uiw/react-markdown-preview';
4
  import { cn } from "@/lib/utils";
5
  import { Copy, Download, Share2, ChevronDown, FileText, FileCode, FileType } from "lucide-react";
6
  import { useState, useRef, useEffect } from 'react';
7
-
8
- interface ContentCanvasProps {
9
- content: string | null;
10
- className?: string;
11
- }
12
-
13
- export function ContentCanvas({ content, className }: ContentCanvasProps) {
14
- const [copied, setCopied] = useState(false);
15
- const [showDownloadMenu, setShowDownloadMenu] = useState(false);
16
- const downloadMenuRef = useRef<HTMLDivElement>(null);
17
- const contentRef = useRef<HTMLDivElement>(null);
18
-
19
- const [showShareMenu, setShowShareMenu] = useState(false);
20
- const shareMenuRef = useRef<HTMLDivElement>(null);
21
-
22
- // Close menu when clicking outside
23
- useEffect(() => {
24
- function handleClickOutside(event: MouseEvent) {
25
- if (downloadMenuRef.current && !downloadMenuRef.current.contains(event.target as Node)) {
26
- setShowDownloadMenu(false);
27
- }
28
- if (shareMenuRef.current && !shareMenuRef.current.contains(event.target as Node)) {
29
- setShowShareMenu(false);
30
- }
31
- }
32
- document.addEventListener("mousedown", handleClickOutside);
33
- return () => {
34
- document.removeEventListener("mousedown", handleClickOutside);
35
- };
36
- }, []);
37
-
38
- const handleCopy = () => {
39
- if (content) {
40
- navigator.clipboard.writeText(content);
41
- setCopied(true);
42
- setTimeout(() => setCopied(false), 2000);
43
- }
44
- }
45
-
46
- const cleanContent = (text: string | null) => {
47
- if (!text) return "";
48
- // Remove leading/trailing markdown code fences
49
- return text.replace(/^```markdown\n/, '').replace(/^```\n/, '').replace(/\n```$/, '');
50
- };
51
-
52
- const downloadMarkdown = () => {
53
- if (!content) return;
54
- const blob = new Blob([cleanContent(content)], { type: 'text/markdown' });
55
- const url = URL.createObjectURL(blob);
56
- const a = document.createElement('a');
57
- a.href = url;
58
- a.download = 'generated-content.md';
59
- document.body.appendChild(a);
60
- a.click();
61
- document.body.removeChild(a);
62
- URL.revokeObjectURL(url);
63
- setShowDownloadMenu(false);
64
- };
65
-
66
- const getHTMLContent = () => {
67
- if (!content || !contentRef.current) return "";
68
- // Wrap content in a basic HTML structure for better viewing
69
- return `<!DOCTYPE html>
70
- <html lang="en">
71
- <head>
72
- <meta charset="UTF-8">
73
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
74
- <title>OmniContent Generation</title>
75
- <style>
76
- body { font-family: system-ui, -apple-system, sans-serif; line-height: 1.6; color: #333; max-width: 800px; margin: 0 auto; padding: 2rem; }
77
- pre { background: #f4f4f4; padding: 1rem; overflow-x: auto; border-radius: 4px; }
78
- code { font-family: monospace; background: #f4f4f4; padding: 0.2rem 0.4rem; border-radius: 4px; }
79
- img { max-width: 100%; height: auto; }
80
- blockquote { border-left: 4px solid #ccc; margin: 0; padding-left: 1rem; color: #666; }
81
- table { border-collapse: collapse; width: 100%; margin: 1rem 0; }
82
- th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
83
- th { background-color: #f2f2f2; }
84
- </style>
85
- </head>
86
- <body>
87
- ${contentRef.current.innerHTML}
88
- </body>
89
- </html>`;
90
- }
91
-
92
- const downloadHTML = () => {
93
- const htmlContent = getHTMLContent();
94
- if (!htmlContent) return;
95
-
96
- const blob = new Blob([htmlContent], { type: 'text/html' });
97
- const url = URL.createObjectURL(blob);
98
- const a = document.createElement('a');
99
- a.href = url;
100
- a.download = 'generated-content.html';
101
- document.body.appendChild(a);
102
- a.click();
103
- document.body.removeChild(a);
104
- URL.revokeObjectURL(url);
105
- setShowDownloadMenu(false);
106
- };
107
-
108
- const handleShare = async (format: 'markdown' | 'html') => {
109
- if (!content) return;
110
-
111
- const shareText = format === 'html' ? getHTMLContent() : cleanContent(content);
112
- const title = format === 'html' ? 'OmniContent Generation (HTML)' : 'OmniContent Generation (Markdown)';
113
-
114
- if (format === 'html') {
115
- // For HTML, we want to share as a FILE if possible, or Copy as Rich Text
116
- // Sharing "text" via navigator.share just shares raw code.
117
- try {
118
- const blob = new Blob([shareText], { type: 'text/html' });
119
- const file = new File([blob], 'omnicontent.html', { type: 'text/html' });
120
-
121
- // 1. Try Native Share with File
122
- if (navigator.canShare && navigator.canShare({ files: [file] })) {
123
- await navigator.share({
124
- files: [file],
125
- title: title,
126
- });
127
- return;
128
- }
129
-
130
- // 2. Fallback: Copy as Rich Text using Clipboard API
131
- // This allows pasting rendered HTML into emails/docs
132
- if (typeof ClipboardItem !== "undefined") {
133
- const data = [new ClipboardItem({
134
- "text/html": blob,
135
- "text/plain": new Blob([cleanContent(content)], { type: 'text/plain' })
136
- })];
137
- await navigator.clipboard.write(data);
138
- alert("HTML copied to clipboard as Rich Text!");
139
- return;
140
- }
141
- } catch (err) {
142
- console.error("Error sharing HTML:", err);
143
- // Fallthrough to basic text copy
144
- }
145
- }
146
-
147
- // Markdown / Text Fallback
148
- // Try Native Share if available (e.g. Mobile, Safari)
149
- if (navigator.share) {
150
- try {
151
- await navigator.share({
152
- title: title,
153
- text: shareText,
154
- });
155
- } catch (err) {
156
- console.log('Error sharing:', err);
157
- }
158
- } else {
159
- // Fallback to clipboard if share not supported
160
- navigator.clipboard.writeText(shareText);
161
- alert(`${format.toUpperCase()} copied to clipboard!`);
162
- }
163
- setShowShareMenu(false);
164
- };
165
-
166
- const handleTwitterShare = () => {
167
- if (!content) return;
168
- // Fallback: Twitter Intent (Must respect 280 char limit)
169
- // We can't share a full article as a Tweet, so we share a teaser.
170
- const preview = cleanContent(content).slice(0, 240); // Leave room for URL/hashtags
171
- const text = encodeURIComponent(`Check out this insights generated by OmniContent!\n\n${preview}...`);
172
- window.open(`https://twitter.com/intent/tweet?text=${text}`, '_blank');
173
- setShowShareMenu(false);
174
- }
175
-
176
-
177
- if (!content) {
178
- return (
179
- <div className={cn("flex items-center justify-center p-8 text-gray-500 border border-dashed border-white/10 rounded-xl h-full", className)}>
180
- <p className="text-sm">Generated content will appear here...</p>
181
- </div>
182
- )
183
- }
184
-
185
-
186
-
187
- return (
188
- <div className={cn("relative flex flex-col h-full bg-[#15171e] rounded-xl border border-white/10 overflow-hidden", className)}>
189
- {/* Toolbar */}
190
- <div className="flex items-center justify-between px-4 py-2 border-b border-white/5 bg-black/20 z-10">
191
- <span className="text-xs font-semibold text-gray-400">Content Preview</span>
192
- <div className="flex items-center gap-2">
193
- <button
194
- onClick={handleCopy}
195
- className="p-1.5 hover:bg-white/10 rounded-md text-gray-400 hover:text-white transition-colors" title="Copy">
196
- <Copy className={cn("w-4 h-4", copied && "text-emerald-500")} />
197
- </button>
198
-
199
- {/* Download Dropdown */}
200
- <div className="relative" ref={downloadMenuRef}>
201
- <button
202
- onClick={() => setShowDownloadMenu(!showDownloadMenu)}
203
- className={cn("p-1.5 hover:bg-white/10 rounded-md text-gray-400 hover:text-white transition-colors flex items-center gap-1", showDownloadMenu && "bg-white/10 text-white")}
204
- title="Download Options">
205
- <Download className="w-4 h-4" />
206
- <ChevronDown className="w-3 h-3" />
207
- </button>
208
-
209
- {showDownloadMenu && (
210
- <div className="absolute right-0 top-full mt-1 w-40 bg-[#1e2028] border border-white/10 rounded-lg shadow-xl overflow-hidden z-50">
211
- <button onClick={downloadMarkdown} className="w-full px-3 py-2 text-left text-sm text-gray-300 hover:bg-white/5 hover:text-white flex items-center gap-2">
212
- <FileCode className="w-4 h-4 text-emerald-400" /> Markdown
213
- </button>
214
- <button onClick={downloadHTML} className="w-full px-3 py-2 text-left text-sm text-gray-300 hover:bg-white/5 hover:text-white flex items-center gap-2">
215
- <FileText className="w-4 h-4 text-blue-400" /> HTML
216
- </button>
217
- <button disabled className="w-full px-3 py-2 text-left text-sm text-gray-600 flex items-center gap-2 cursor-not-allowed opacity-50">
218
- <FileType className="w-4 h-4" /> PDF (Coming Soon)
219
- </button>
220
- </div>
221
- )}
222
- </div>
223
-
224
- {/* Share Dropdown */}
225
- <div className="relative" ref={shareMenuRef}>
226
- <button
227
- onClick={() => setShowShareMenu(!showShareMenu)}
228
- className={cn("p-1.5 hover:bg-white/10 rounded-md text-gray-400 hover:text-white transition-colors flex items-center gap-1", showShareMenu && "bg-white/10 text-white")}
229
- title="Share Options">
230
- <Share2 className="w-4 h-4" />
231
- <ChevronDown className="w-3 h-3" />
232
- </button>
233
-
234
- {showShareMenu && (
235
- <div className="absolute right-0 top-full mt-1 w-44 bg-[#1e2028] border border-white/10 rounded-lg shadow-xl overflow-hidden z-50">
236
- <button onClick={() => handleShare('markdown')} className="w-full px-3 py-2 text-left text-sm text-gray-300 hover:bg-white/5 hover:text-white flex items-center gap-2">
237
- <FileCode className="w-4 h-4 text-emerald-400" /> Share Markdown
238
- </button>
239
- <button onClick={() => handleShare('html')} className="w-full px-3 py-2 text-left text-sm text-gray-300 hover:bg-white/5 hover:text-white flex items-center gap-2">
240
- <FileText className="w-4 h-4 text-blue-400" /> Share HTML
241
- </button>
242
- <div className="h-[1px] bg-white/5 my-1" />
243
- <button onClick={handleTwitterShare} className="w-full px-3 py-2 text-left text-sm text-gray-300 hover:bg-white/5 hover:text-white flex items-center gap-2">
244
- <svg viewBox="0 0 24 24" className="w-4 h-4 text-white fill-current"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" /></svg>
245
- Share on X
246
- </button>
247
- </div>
248
- )}
249
- </div>
250
- </div>
251
- </div>
252
-
253
- {/* Editor Area */}
254
- <div className="flex-1 overflow-auto p-6 scrollbar-thin scrollbar-thumb-white/10 scrollbar-track-transparent">
255
- <div ref={contentRef} className="prose prose-invert lg:prose-xl max-w-none prose-headings:text-[#34d399] prose-a:text-[#60a5fa]">
256
- <MarkdownPreview
257
- source={cleanContent(content)}
258
- style={{ backgroundColor: 'transparent', color: 'inherit', fontFamily: 'inherit' }}
259
- wrapperElement={{
260
- "data-color-mode": "dark"
261
- }}
262
- />
263
- </div>
264
- </div>
265
- </div>
266
- );
267
- }
 
1
+ "use client";
2
+
3
  import MarkdownPreview from '@uiw/react-markdown-preview';
4
  import { cn } from "@/lib/utils";
5
  import { Copy, Download, Share2, ChevronDown, FileText, FileCode, FileType } from "lucide-react";
6
  import { useState, useRef, useEffect } from 'react';
7
+
8
+ interface ContentCanvasProps {
9
+ content: string | null;
10
+ className?: string;
11
+ }
12
+
13
+ export function ContentCanvas({ content, className }: ContentCanvasProps) {
14
+ const [copied, setCopied] = useState(false);
15
+ const [showDownloadMenu, setShowDownloadMenu] = useState(false);
16
+ const downloadMenuRef = useRef<HTMLDivElement>(null);
17
+ const contentRef = useRef<HTMLDivElement>(null);
18
+
19
+ const [showShareMenu, setShowShareMenu] = useState(false);
20
+ const shareMenuRef = useRef<HTMLDivElement>(null);
21
+
22
+ // Close menu when clicking outside
23
+ useEffect(() => {
24
+ function handleClickOutside(event: MouseEvent) {
25
+ if (downloadMenuRef.current && !downloadMenuRef.current.contains(event.target as Node)) {
26
+ setShowDownloadMenu(false);
27
+ }
28
+ if (shareMenuRef.current && !shareMenuRef.current.contains(event.target as Node)) {
29
+ setShowShareMenu(false);
30
+ }
31
+ }
32
+ document.addEventListener("mousedown", handleClickOutside);
33
+ return () => {
34
+ document.removeEventListener("mousedown", handleClickOutside);
35
+ };
36
+ }, []);
37
+
38
+ const handleCopy = () => {
39
+ if (content) {
40
+ navigator.clipboard.writeText(content);
41
+ setCopied(true);
42
+ setTimeout(() => setCopied(false), 2000);
43
+ }
44
+ }
45
+
46
+ const cleanContent = (text: string | null) => {
47
+ if (!text) return "";
48
+ // Remove leading/trailing markdown code fences
49
+ return text.replace(/^```markdown\n/, '').replace(/^```\n/, '').replace(/\n```$/, '');
50
+ };
51
+
52
+ const downloadMarkdown = () => {
53
+ if (!content) return;
54
+ const blob = new Blob([cleanContent(content)], { type: 'text/markdown' });
55
+ const url = URL.createObjectURL(blob);
56
+ const a = document.createElement('a');
57
+ a.href = url;
58
+ a.download = 'generated-content.md';
59
+ document.body.appendChild(a);
60
+ a.click();
61
+ document.body.removeChild(a);
62
+ URL.revokeObjectURL(url);
63
+ setShowDownloadMenu(false);
64
+ };
65
+
66
+ const getHTMLContent = () => {
67
+ if (!content || !contentRef.current) return "";
68
+ // Wrap content in a basic HTML structure for better viewing
69
+ return `<!DOCTYPE html>
70
+ <html lang="en">
71
+ <head>
72
+ <meta charset="UTF-8">
73
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
74
+ <title>OmniContent Generation</title>
75
+ <style>
76
+ body { font-family: system-ui, -apple-system, sans-serif; line-height: 1.6; color: #333; max-width: 800px; margin: 0 auto; padding: 2rem; }
77
+ pre { background: #f4f4f4; padding: 1rem; overflow-x: auto; border-radius: 4px; }
78
+ code { font-family: monospace; background: #f4f4f4; padding: 0.2rem 0.4rem; border-radius: 4px; }
79
+ img { max-width: 100%; height: auto; }
80
+ blockquote { border-left: 4px solid #ccc; margin: 0; padding-left: 1rem; color: #666; }
81
+ table { border-collapse: collapse; width: 100%; margin: 1rem 0; }
82
+ th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
83
+ th { background-color: #f2f2f2; }
84
+ </style>
85
+ </head>
86
+ <body>
87
+ ${contentRef.current.innerHTML}
88
+ </body>
89
+ </html>`;
90
+ }
91
+
92
+ const downloadHTML = () => {
93
+ const htmlContent = getHTMLContent();
94
+ if (!htmlContent) return;
95
+
96
+ const blob = new Blob([htmlContent], { type: 'text/html' });
97
+ const url = URL.createObjectURL(blob);
98
+ const a = document.createElement('a');
99
+ a.href = url;
100
+ a.download = 'generated-content.html';
101
+ document.body.appendChild(a);
102
+ a.click();
103
+ document.body.removeChild(a);
104
+ URL.revokeObjectURL(url);
105
+ setShowDownloadMenu(false);
106
+ };
107
+
108
+ const handleShare = async (format: 'markdown' | 'html') => {
109
+ if (!content) return;
110
+
111
+ const shareText = format === 'html' ? getHTMLContent() : cleanContent(content);
112
+ const title = format === 'html' ? 'OmniContent Generation (HTML)' : 'OmniContent Generation (Markdown)';
113
+
114
+ if (format === 'html') {
115
+ // For HTML, we want to share as a FILE if possible, or Copy as Rich Text
116
+ // Sharing "text" via navigator.share just shares raw code.
117
+ try {
118
+ const blob = new Blob([shareText], { type: 'text/html' });
119
+ const file = new File([blob], 'omnicontent.html', { type: 'text/html' });
120
+
121
+ // 1. Try Native Share with File
122
+ if (navigator.canShare && navigator.canShare({ files: [file] })) {
123
+ await navigator.share({
124
+ files: [file],
125
+ title: title,
126
+ });
127
+ return;
128
+ }
129
+
130
+ // 2. Fallback: Copy as Rich Text using Clipboard API
131
+ // This allows pasting rendered HTML into emails/docs
132
+ if (typeof ClipboardItem !== "undefined") {
133
+ const data = [new ClipboardItem({
134
+ "text/html": blob,
135
+ "text/plain": new Blob([cleanContent(content)], { type: 'text/plain' })
136
+ })];
137
+ await navigator.clipboard.write(data);
138
+ alert("HTML copied to clipboard as Rich Text!");
139
+ return;
140
+ }
141
+ } catch (err) {
142
+ console.error("Error sharing HTML:", err);
143
+ // Fallthrough to basic text copy
144
+ }
145
+ }
146
+
147
+ // Markdown / Text Fallback
148
+ // Try Native Share if available (e.g. Mobile, Safari)
149
+ if (navigator.share) {
150
+ try {
151
+ await navigator.share({
152
+ title: title,
153
+ text: shareText,
154
+ });
155
+ } catch (err) {
156
+ console.log('Error sharing:', err);
157
+ }
158
+ } else {
159
+ // Fallback to clipboard if share not supported
160
+ navigator.clipboard.writeText(shareText);
161
+ alert(`${format.toUpperCase()} copied to clipboard!`);
162
+ }
163
+ setShowShareMenu(false);
164
+ };
165
+
166
+ const handleTwitterShare = () => {
167
+ if (!content) return;
168
+ // Fallback: Twitter Intent (Must respect 280 char limit)
169
+ // We can't share a full article as a Tweet, so we share a teaser.
170
+ const preview = cleanContent(content).slice(0, 240); // Leave room for URL/hashtags
171
+ const text = encodeURIComponent(`Check out this insights generated by OmniContent!\n\n${preview}...`);
172
+ window.open(`https://twitter.com/intent/tweet?text=${text}`, '_blank');
173
+ setShowShareMenu(false);
174
+ }
175
+
176
+
177
+ if (!content) {
178
+ return (
179
+ <div className={cn("flex items-center justify-center p-8 text-gray-500 border border-dashed border-white/10 rounded-xl h-full", className)}>
180
+ <p className="text-sm">Generated content will appear here...</p>
181
+ </div>
182
+ )
183
+ }
184
+
185
+
186
+
187
+ return (
188
+ <div className={cn("relative flex flex-col h-full bg-[#15171e] rounded-xl border border-white/10 overflow-hidden", className)}>
189
+ {/* Toolbar */}
190
+ <div className="flex items-center justify-between px-4 py-2 border-b border-white/5 bg-black/20 z-10">
191
+ <span className="text-xs font-semibold text-gray-400">Content Preview</span>
192
+ <div className="flex items-center gap-2">
193
+ <button
194
+ onClick={handleCopy}
195
+ className="p-1.5 hover:bg-white/10 rounded-md text-gray-400 hover:text-white transition-colors" title="Copy">
196
+ <Copy className={cn("w-4 h-4", copied && "text-emerald-500")} />
197
+ </button>
198
+
199
+ {/* Download Dropdown */}
200
+ <div className="relative" ref={downloadMenuRef}>
201
+ <button
202
+ onClick={() => setShowDownloadMenu(!showDownloadMenu)}
203
+ className={cn("p-1.5 hover:bg-white/10 rounded-md text-gray-400 hover:text-white transition-colors flex items-center gap-1", showDownloadMenu && "bg-white/10 text-white")}
204
+ title="Download Options">
205
+ <Download className="w-4 h-4" />
206
+ <ChevronDown className="w-3 h-3" />
207
+ </button>
208
+
209
+ {showDownloadMenu && (
210
+ <div className="absolute right-0 top-full mt-1 w-40 bg-[#1e2028] border border-white/10 rounded-lg shadow-xl overflow-hidden z-50">
211
+ <button onClick={downloadMarkdown} className="w-full px-3 py-2 text-left text-sm text-gray-300 hover:bg-white/5 hover:text-white flex items-center gap-2">
212
+ <FileCode className="w-4 h-4 text-emerald-400" /> Markdown
213
+ </button>
214
+ <button onClick={downloadHTML} className="w-full px-3 py-2 text-left text-sm text-gray-300 hover:bg-white/5 hover:text-white flex items-center gap-2">
215
+ <FileText className="w-4 h-4 text-blue-400" /> HTML
216
+ </button>
217
+ <button disabled className="w-full px-3 py-2 text-left text-sm text-gray-600 flex items-center gap-2 cursor-not-allowed opacity-50">
218
+ <FileType className="w-4 h-4" /> PDF (Coming Soon)
219
+ </button>
220
+ </div>
221
+ )}
222
+ </div>
223
+
224
+ {/* Share Dropdown */}
225
+ <div className="relative" ref={shareMenuRef}>
226
+ <button
227
+ onClick={() => setShowShareMenu(!showShareMenu)}
228
+ className={cn("p-1.5 hover:bg-white/10 rounded-md text-gray-400 hover:text-white transition-colors flex items-center gap-1", showShareMenu && "bg-white/10 text-white")}
229
+ title="Share Options">
230
+ <Share2 className="w-4 h-4" />
231
+ <ChevronDown className="w-3 h-3" />
232
+ </button>
233
+
234
+ {showShareMenu && (
235
+ <div className="absolute right-0 top-full mt-1 w-44 bg-[#1e2028] border border-white/10 rounded-lg shadow-xl overflow-hidden z-50">
236
+ <button onClick={() => handleShare('markdown')} className="w-full px-3 py-2 text-left text-sm text-gray-300 hover:bg-white/5 hover:text-white flex items-center gap-2">
237
+ <FileCode className="w-4 h-4 text-emerald-400" /> Share Markdown
238
+ </button>
239
+ <button onClick={() => handleShare('html')} className="w-full px-3 py-2 text-left text-sm text-gray-300 hover:bg-white/5 hover:text-white flex items-center gap-2">
240
+ <FileText className="w-4 h-4 text-blue-400" /> Share HTML
241
+ </button>
242
+ <div className="h-[1px] bg-white/5 my-1" />
243
+ <button onClick={handleTwitterShare} className="w-full px-3 py-2 text-left text-sm text-gray-300 hover:bg-white/5 hover:text-white flex items-center gap-2">
244
+ <svg viewBox="0 0 24 24" className="w-4 h-4 text-white fill-current"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" /></svg>
245
+ Share on X
246
+ </button>
247
+ </div>
248
+ )}
249
+ </div>
250
+ </div>
251
+ </div>
252
+
253
+ {/* Editor Area */}
254
+ <div className="flex-1 overflow-auto p-6 scrollbar-thin scrollbar-thumb-white/10 scrollbar-track-transparent">
255
+ <div ref={contentRef} className="prose prose-invert lg:prose-xl max-w-none prose-headings:text-[#34d399] prose-a:text-[#60a5fa]">
256
+ <MarkdownPreview
257
+ source={cleanContent(content)}
258
+ style={{ backgroundColor: 'transparent', color: 'inherit', fontFamily: 'inherit' }}
259
+ wrapperElement={{
260
+ "data-color-mode": "dark"
261
+ }}
262
+ />
263
+ </div>
264
+ </div>
265
+ </div>
266
+ );
267
+ }
web/components/GeneratorApp.tsx ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { AgentPulse } from "@/components/AgentPulse";
5
+ import { ConfigPanel } from "@/components/ConfigPanel";
6
+ import { ContentCanvas } from "@/components/ContentCanvas";
7
+ import { RateLimitModal } from "@/components/RateLimitModal";
8
+ import { Sidebar } from "@/components/Sidebar";
9
+ import { useAgentJob } from "@/hooks/useAgentJob";
10
+ import { DOMAINS, DomainKey } from "@/lib/domains";
11
+ import { cn } from "@/lib/utils";
12
+ import { useRouter } from "next/navigation";
13
+
14
+ export function GeneratorApp() {
15
+ const [selectedDomain, setSelectedDomain] = useState<DomainKey>("fintech");
16
+ const [topic, setTopic] = useState("");
17
+ const [tone, setTone] = useState("Professional");
18
+ const router = useRouter();
19
+
20
+ const { status, result, logs, usage, error, rateLimit, startJob, clearRateLimit } = useAgentJob();
21
+ const [showAgentPulse, setShowAgentPulse] = useState(false);
22
+
23
+ const handleGenerate = () => {
24
+ setShowAgentPulse(true);
25
+ startJob(topic, selectedDomain, tone);
26
+ };
27
+
28
+ return (
29
+ <div className="flex min-h-screen bg-[#0B0E14] text-gray-100 font-sans selection:bg-emerald-500/30">
30
+ <Sidebar selectedDomain={selectedDomain} onSelectDomain={setSelectedDomain} />
31
+
32
+ <main className="relative flex flex-1 flex-col overflow-hidden">
33
+ <div
34
+ className={cn(
35
+ "pointer-events-none absolute top-0 left-0 h-[500px] w-full bg-gradient-to-b opacity-10 transition-colors duration-700",
36
+ DOMAINS[selectedDomain].accent.replace("text-", "from-"),
37
+ )}
38
+ />
39
+
40
+ <div className="z-10 flex flex-1 flex-col gap-8 overflow-y-auto p-8">
41
+ <div className="flex-none">
42
+ <ConfigPanel
43
+ domain={selectedDomain}
44
+ topic={topic}
45
+ setTopic={setTopic}
46
+ tone={tone}
47
+ setTone={setTone}
48
+ onGenerate={handleGenerate}
49
+ isGenerating={status === "PENDING" || status === "STARTED"}
50
+ runsLeft={usage?.remaining}
51
+ runLimit={usage?.limit}
52
+ resetInSeconds={usage?.reset_in_seconds}
53
+ mode={usage?.mode}
54
+ />
55
+ {error && !rateLimit && <p className="mt-3 text-sm text-rose-300">{error}</p>}
56
+ </div>
57
+
58
+ <div className="flex min-h-[500px] flex-1 gap-6">
59
+ <ContentCanvas content={result} className="w-full shadow-2xl" />
60
+ </div>
61
+ </div>
62
+
63
+ {showAgentPulse && (
64
+ <AgentPulse
65
+ logs={logs}
66
+ status={status}
67
+ onClose={() => setShowAgentPulse(false)}
68
+ />
69
+ )}
70
+
71
+ <RateLimitModal
72
+ open={Boolean(rateLimit)}
73
+ message={rateLimit?.message ?? "Daily limit reached."}
74
+ resetInSeconds={rateLimit?.reset_in_seconds ?? 0}
75
+ onClose={clearRateLimit}
76
+ onLogin={() => {
77
+ clearRateLimit();
78
+ router.push("/login");
79
+ }}
80
+ />
81
+ </main>
82
+ </div>
83
+ );
84
+ }
web/components/Providers.tsx ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { SessionProvider } from "next-auth/react";
4
+
5
+ export function Providers({ children }: { children: React.ReactNode }) {
6
+ return <SessionProvider>{children}</SessionProvider>;
7
+ }
web/components/RateLimitModal.tsx ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useMemo } from "react";
4
+
5
+ interface RateLimitModalProps {
6
+ open: boolean;
7
+ message: string;
8
+ resetInSeconds: number;
9
+ onClose: () => void;
10
+ onLogin: () => void;
11
+ }
12
+
13
+ export function RateLimitModal({
14
+ open,
15
+ message,
16
+ resetInSeconds,
17
+ onClose,
18
+ onLogin,
19
+ }: RateLimitModalProps) {
20
+ const resetLabel = useMemo(() => {
21
+ const total = Math.max(resetInSeconds, 0);
22
+ const hours = Math.floor(total / 3600);
23
+ const mins = Math.ceil((total % 3600) / 60);
24
+ if (hours > 0) return `${hours}h ${mins}m`;
25
+ return `${mins}m`;
26
+ }, [resetInSeconds]);
27
+
28
+ if (!open) return null;
29
+
30
+ return (
31
+ <div className="fixed inset-0 z-[70] flex items-center justify-center bg-black/60 p-4">
32
+ <div className="w-full max-w-md rounded-xl border border-white/10 bg-[#15171e] p-5 shadow-2xl">
33
+ <h3 className="text-lg font-semibold text-white">Limit Reached</h3>
34
+ <p className="mt-2 text-sm text-gray-300">{message}</p>
35
+ <p className="mt-1 text-xs text-amber-300">Try again in about {resetLabel}.</p>
36
+ <div className="mt-5 flex items-center justify-end gap-2">
37
+ <button
38
+ onClick={onClose}
39
+ className="rounded-lg border border-white/15 bg-white/5 px-3 py-2 text-sm text-gray-200 hover:bg-white/10"
40
+ >
41
+ Close
42
+ </button>
43
+ <button
44
+ onClick={onLogin}
45
+ className="rounded-lg border border-emerald-400/30 bg-emerald-500/20 px-3 py-2 text-sm text-emerald-100 hover:bg-emerald-500/30"
46
+ >
47
+ Login
48
+ </button>
49
+ </div>
50
+ </div>
51
+ </div>
52
+ );
53
+ }
web/components/Sidebar.tsx CHANGED
@@ -1,15 +1,15 @@
1
- "use client";
2
-
3
  import { DOMAINS, DomainKey } from "@/lib/domains";
4
  import { cn } from "@/lib/utils";
5
  import { History } from "lucide-react";
6
  import { signIn, signOut, useSession } from "next-auth/react";
7
-
8
- interface SidebarProps {
9
- selectedDomain: DomainKey;
10
- onSelectDomain: (domain: DomainKey) => void;
11
- }
12
-
13
  export function Sidebar({ selectedDomain, onSelectDomain }: SidebarProps) {
14
  const { data: session, status } = useSession();
15
  const isAuthenticated = status === "authenticated";
@@ -17,40 +17,40 @@ export function Sidebar({ selectedDomain, onSelectDomain }: SidebarProps) {
17
 
18
  return (
19
  <div className="w-64 border-r border-white/10 bg-[#0B0E14] flex flex-col h-screen p-4">
20
- <div className="mb-8 px-2">
21
- <h1 className="text-xl font-bold bg-gradient-to-r from-white to-gray-400 bg-clip-text text-transparent">
22
- OmniContent
23
- </h1>
24
- <p className="text-xs text-gray-500">Multi-Agent Engine</p>
25
- </div>
26
-
27
- <div className="space-y-1 flex-1">
28
- <p className="px-2 text-xs font-semibold text-gray-500 mb-2 uppercase tracking-wider">
29
- Expert Domains
30
- </p>
31
- {(Object.keys(DOMAINS) as DomainKey[]).map((key) => {
32
- const domain = DOMAINS[key];
33
- const isSelected = selectedDomain === key;
34
- const Icon = domain.icon;
35
-
36
- return (
37
- <button
38
- key={key}
39
- onClick={() => onSelectDomain(key)}
40
- className={cn(
41
- "w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm transition-all duration-200",
42
- isSelected
43
- ? cn(domain.bg, domain.accent, "font-medium")
44
- : "text-gray-400 hover:text-gray-200 hover:bg-white/5"
45
- )}
46
- >
47
- <Icon className="w-4 h-4" />
48
- {domain.label}
49
- </button>
50
- );
51
- })}
52
- </div>
53
-
54
  <div className="pt-4 border-t border-white/10 space-y-2">
55
  {isAuthenticated ? (
56
  <div className="rounded-lg border border-emerald-400/20 bg-emerald-500/10 px-3 py-2">
@@ -91,8 +91,8 @@ export function Sidebar({ selectedDomain, onSelectDomain }: SidebarProps) {
91
  <button className="w-full flex items-center gap-3 px-3 py-2 text-gray-400 hover:text-white text-sm transition-colors">
92
  <History className="w-4 h-4" />
93
  History
94
- </button>
95
- </div>
96
- </div>
97
- );
98
- }
 
1
+ "use client";
2
+
3
  import { DOMAINS, DomainKey } from "@/lib/domains";
4
  import { cn } from "@/lib/utils";
5
  import { History } from "lucide-react";
6
  import { signIn, signOut, useSession } from "next-auth/react";
7
+
8
+ interface SidebarProps {
9
+ selectedDomain: DomainKey;
10
+ onSelectDomain: (domain: DomainKey) => void;
11
+ }
12
+
13
  export function Sidebar({ selectedDomain, onSelectDomain }: SidebarProps) {
14
  const { data: session, status } = useSession();
15
  const isAuthenticated = status === "authenticated";
 
17
 
18
  return (
19
  <div className="w-64 border-r border-white/10 bg-[#0B0E14] flex flex-col h-screen p-4">
20
+ <div className="mb-8 px-2">
21
+ <h1 className="text-xl font-bold bg-gradient-to-r from-white to-gray-400 bg-clip-text text-transparent">
22
+ OmniContent
23
+ </h1>
24
+ <p className="text-xs text-gray-500">Multi-Agent Engine</p>
25
+ </div>
26
+
27
+ <div className="space-y-1 flex-1">
28
+ <p className="px-2 text-xs font-semibold text-gray-500 mb-2 uppercase tracking-wider">
29
+ Expert Domains
30
+ </p>
31
+ {(Object.keys(DOMAINS) as DomainKey[]).map((key) => {
32
+ const domain = DOMAINS[key];
33
+ const isSelected = selectedDomain === key;
34
+ const Icon = domain.icon;
35
+
36
+ return (
37
+ <button
38
+ key={key}
39
+ onClick={() => onSelectDomain(key)}
40
+ className={cn(
41
+ "w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm transition-all duration-200",
42
+ isSelected
43
+ ? cn(domain.bg, domain.accent, "font-medium")
44
+ : "text-gray-400 hover:text-gray-200 hover:bg-white/5"
45
+ )}
46
+ >
47
+ <Icon className="w-4 h-4" />
48
+ {domain.label}
49
+ </button>
50
+ );
51
+ })}
52
+ </div>
53
+
54
  <div className="pt-4 border-t border-white/10 space-y-2">
55
  {isAuthenticated ? (
56
  <div className="rounded-lg border border-emerald-400/20 bg-emerald-500/10 px-3 py-2">
 
91
  <button className="w-full flex items-center gap-3 px-3 py-2 text-gray-400 hover:text-white text-sm transition-colors">
92
  <History className="w-4 h-4" />
93
  History
94
+ </button>
95
+ </div>
96
+ </div>
97
+ );
98
+ }