aboutsummaryrefslogtreecommitdiffstats
path: root/renderer/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'renderer/src/components')
-rw-r--r--renderer/src/components/ActionBar.tsx97
-rw-r--r--renderer/src/components/ChatPane.tsx66
-rw-r--r--renderer/src/components/DocumentPane.tsx104
-rw-r--r--renderer/src/components/Header.tsx99
4 files changed, 366 insertions, 0 deletions
diff --git a/renderer/src/components/ActionBar.tsx b/renderer/src/components/ActionBar.tsx
new file mode 100644
index 0000000..22f34b4
--- /dev/null
+++ b/renderer/src/components/ActionBar.tsx
@@ -0,0 +1,97 @@
1import React from "react";
2import type { Phase, PermissionMode, TokenUsage } from "../types";
3
4interface ActionBarProps {
5 phase: Phase;
6 hasChanges: boolean;
7 isLoading: boolean;
8 tokenUsage: TokenUsage;
9 permissionMode: PermissionMode;
10 onReview: () => void;
11 onSubmit: () => void;
12 onPermissionModeChange: (mode: PermissionMode) => void;
13 disabled: boolean;
14}
15
16export function ActionBar({
17 phase,
18 hasChanges,
19 isLoading,
20 tokenUsage,
21 permissionMode,
22 onReview,
23 onSubmit,
24 onPermissionModeChange,
25 disabled,
26}: ActionBarProps) {
27 const totalTokens = tokenUsage.inputTokens + tokenUsage.outputTokens;
28 const maxTokens = 200000;
29 const usagePercent = Math.min((totalTokens / maxTokens) * 100, 100);
30
31 const getBarColor = () => {
32 if (usagePercent > 80) return "#ef4444";
33 if (usagePercent > 50) return "#f59e0b";
34 return "#10b981";
35 };
36
37 return (
38 <div className="action-bar">
39 <div className="action-bar-left">
40 <div className="token-indicator">
41 <div className="token-bar">
42 <div
43 className="token-fill"
44 style={{
45 width: `${usagePercent}%`,
46 backgroundColor: getBarColor(),
47 }}
48 />
49 </div>
50 <span className="token-label">
51 {(totalTokens / 1000).toFixed(1)}k / 200k
52 </span>
53 </div>
54
55 {phase === "implement" && (
56 <label className="permission-toggle">
57 <input
58 type="checkbox"
59 checked={permissionMode === "bypassPermissions"}
60 onChange={(e) =>
61 onPermissionModeChange(
62 e.target.checked ? "bypassPermissions" : "acceptEdits"
63 )
64 }
65 disabled={disabled}
66 />
67 Bypass Permissions
68 </label>
69 )}
70 </div>
71
72 <div className="action-bar-right">
73 {phase !== "implement" && (
74 <>
75 <button
76 onClick={onReview}
77 disabled={disabled || isLoading || !hasChanges}
78 className="btn-secondary"
79 >
80 Review
81 </button>
82 <button
83 onClick={onSubmit}
84 disabled={disabled || isLoading}
85 className="btn-primary"
86 >
87 Submit →
88 </button>
89 </>
90 )}
91 {phase === "implement" && isLoading && (
92 <span className="implementing-status">Implementing...</span>
93 )}
94 </div>
95 </div>
96 );
97}
diff --git a/renderer/src/components/ChatPane.tsx b/renderer/src/components/ChatPane.tsx
new file mode 100644
index 0000000..917d462
--- /dev/null
+++ b/renderer/src/components/ChatPane.tsx
@@ -0,0 +1,66 @@
1import React, { useState, useRef, useEffect } from "react";
2import type { Message } from "../types";
3
4interface ChatPaneProps {
5 messages: Message[];
6 onSend: (message: string) => void;
7 isLoading: boolean;
8 disabled: boolean;
9 placeholder: string;
10}
11
12export function ChatPane({
13 messages,
14 onSend,
15 isLoading,
16 disabled,
17 placeholder,
18}: ChatPaneProps) {
19 const [input, setInput] = useState("");
20 const messagesEndRef = useRef<HTMLDivElement>(null);
21
22 useEffect(() => {
23 messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
24 }, [messages]);
25
26 const handleSend = () => {
27 if (!input.trim() || isLoading || disabled) return;
28 onSend(input.trim());
29 setInput("");
30 };
31
32 return (
33 <div className="chat-pane">
34 <div className="chat-messages">
35 {messages.map((msg) => (
36 <div key={msg.id} className={`message ${msg.role}`}>
37 <div className="message-content">{msg.content}</div>
38 </div>
39 ))}
40 {isLoading && (
41 <div className="message assistant loading">
42 <div className="message-content">Thinking...</div>
43 </div>
44 )}
45 <div ref={messagesEndRef} />
46 </div>
47
48 <div className="chat-input">
49 <input
50 type="text"
51 value={input}
52 onChange={(e) => setInput(e.target.value)}
53 onKeyDown={(e) => e.key === "Enter" && !e.shiftKey && handleSend()}
54 placeholder={placeholder}
55 disabled={disabled || isLoading}
56 />
57 <button
58 onClick={handleSend}
59 disabled={disabled || isLoading || !input.trim()}
60 >
61 Send
62 </button>
63 </div>
64 </div>
65 );
66}
diff --git a/renderer/src/components/DocumentPane.tsx b/renderer/src/components/DocumentPane.tsx
new file mode 100644
index 0000000..e44b0fb
--- /dev/null
+++ b/renderer/src/components/DocumentPane.tsx
@@ -0,0 +1,104 @@
1import React, { useState, useMemo } from "react";
2import type { Phase } from "../types";
3
4interface DocumentPaneProps {
5 content: string;
6 onChange: (content: string) => void;
7 phase: Phase;
8 disabled: boolean;
9}
10
11function renderMarkdown(md: string): string {
12 return (
13 md
14 // Headers
15 .replace(/^### (.*$)/gm, "<h3>$1</h3>")
16 .replace(/^## (.*$)/gm, "<h2>$1</h2>")
17 .replace(/^# (.*$)/gm, "<h1>$1</h1>")
18 // Bold/italic
19 .replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
20 .replace(/\*([^*]+)\*/g, "<em>$1</em>")
21 // Code blocks
22 .replace(
23 /```(\w*)\n([\s\S]*?)```/g,
24 '<pre><code class="language-$1">$2</code></pre>'
25 )
26 .replace(/`([^`]+)`/g, "<code>$1</code>")
27 // Lists
28 .replace(/^- \[x\] (.*$)/gm, '<li class="task done">☑ $1</li>')
29 .replace(/^- \[ \] (.*$)/gm, '<li class="task">☐ $1</li>')
30 .replace(/^- (.*$)/gm, "<li>$1</li>")
31 // Review comments (highlight them)
32 .replace(/(\/\/ REVIEW:.*$)/gm, '<mark class="review">$1</mark>')
33 .replace(/(\/\/ NOTE:.*$)/gm, '<mark class="note">$1</mark>')
34 // Paragraphs
35 .replace(/\n\n/g, "</p><p>")
36 .replace(/^(.+)$/gm, "<p>$1</p>")
37 // Clean up
38 .replace(/<p><\/p>/g, "")
39 .replace(/<p>(<h[1-3]>)/g, "$1")
40 .replace(/(<\/h[1-3]>)<\/p>/g, "$1")
41 .replace(/<p>(<pre>)/g, "$1")
42 .replace(/(<\/pre>)<\/p>/g, "$1")
43 .replace(/<p>(<li)/g, "$1")
44 .replace(/(<\/li>)<\/p>/g, "$1")
45 );
46}
47
48export function DocumentPane({
49 content,
50 onChange,
51 phase,
52 disabled,
53}: DocumentPaneProps) {
54 const [isEditing, setIsEditing] = useState(false);
55 const renderedHtml = useMemo(() => renderMarkdown(content), [content]);
56
57 if (phase === "implement") {
58 return (
59 <div className="document-pane">
60 <div className="document-header">
61 <span>plan.md</span>
62 <span className="badge">Implementing...</span>
63 </div>
64 <div
65 className="document-content rendered"
66 dangerouslySetInnerHTML={{ __html: renderedHtml }}
67 />
68 </div>
69 );
70 }
71
72 const filename = phase === "research" ? "research.md" : "plan.md";
73
74 return (
75 <div className="document-pane">
76 <div className="document-header">
77 <span>{filename}</span>
78 <button onClick={() => setIsEditing(!isEditing)}>
79 {isEditing ? "Preview" : "Edit"}
80 </button>
81 </div>
82
83 {isEditing ? (
84 <textarea
85 className="document-content editing"
86 value={content}
87 onChange={(e) => onChange(e.target.value)}
88 disabled={disabled}
89 placeholder={`${filename} will appear here...`}
90 />
91 ) : (
92 <div
93 className="document-content rendered"
94 dangerouslySetInnerHTML={{
95 __html:
96 renderedHtml ||
97 '<p class="empty">Document will appear here after Claude generates it...</p>',
98 }}
99 onClick={() => !disabled && setIsEditing(true)}
100 />
101 )}
102 </div>
103 );
104}
diff --git a/renderer/src/components/Header.tsx b/renderer/src/components/Header.tsx
new file mode 100644
index 0000000..3dcbba9
--- /dev/null
+++ b/renderer/src/components/Header.tsx
@@ -0,0 +1,99 @@
1import React from "react";
2import type { Project, Session, Phase } from "../types";
3
4interface HeaderProps {
5 projects: Project[];
6 sessions: Session[];
7 selectedProject: Project | null;
8 selectedSession: Session | null;
9 onSelectProject: (project: Project | null) => void;
10 onSelectSession: (session: Session | null) => void;
11 onCreateProject: () => void;
12 onCreateSession: () => void;
13}
14
15const phaseLabels: Record<Phase, string> = {
16 research: "Research",
17 plan: "Plan",
18 implement: "Implement",
19};
20
21const phases: Phase[] = ["research", "plan", "implement"];
22
23export function Header({
24 projects,
25 sessions,
26 selectedProject,
27 selectedSession,
28 onSelectProject,
29 onSelectSession,
30 onCreateProject,
31 onCreateSession,
32}: HeaderProps) {
33 return (
34 <header className="header">
35 <div className="header-left">
36 <select
37 value={selectedProject?.id || ""}
38 onChange={(e) => {
39 const project = projects.find((p) => p.id === e.target.value);
40 onSelectProject(project || null);
41 onSelectSession(null);
42 }}
43 >
44 <option value="">Select Project...</option>
45 {projects.map((p) => (
46 <option key={p.id} value={p.id}>
47 {p.name}
48 </option>
49 ))}
50 </select>
51 <button onClick={onCreateProject}>+ Project</button>
52
53 {selectedProject && (
54 <>
55 <select
56 value={selectedSession?.id || ""}
57 onChange={(e) => {
58 const session = sessions.find((s) => s.id === e.target.value);
59 onSelectSession(session || null);
60 }}
61 >
62 <option value="">Select Session...</option>
63 {sessions.map((s) => (
64 <option key={s.id} value={s.id}>
65 {s.name}
66 </option>
67 ))}
68 </select>
69 <button onClick={onCreateSession}>+ Session</button>
70 </>
71 )}
72 </div>
73
74 <div className="header-right">
75 {selectedSession && (
76 <div className="phase-indicator">
77 {phases.map((phase) => {
78 const phaseIndex = phases.indexOf(phase);
79 const currentIndex = phases.indexOf(selectedSession.phase);
80 const isComplete = phaseIndex < currentIndex;
81 const isActive = phase === selectedSession.phase;
82
83 return (
84 <span
85 key={phase}
86 className={`phase-step ${isActive ? "active" : ""} ${
87 isComplete ? "complete" : ""
88 }`}
89 >
90 {phaseLabels[phase]}
91 </span>
92 );
93 })}
94 </div>
95 )}
96 </div>
97 </header>
98 );
99}