feat: ui polish (sticky header, dark mode, flat inputs)

This commit is contained in:
Dave
2025-12-25 12:58:37 +00:00
parent dfefa3a8bd
commit c3ff2bdebd
6 changed files with 407 additions and 228 deletions

View File

@@ -1,116 +1,116 @@
.logo.vite:hover {
filter: drop-shadow(0 0 2em #747bff);
filter: drop-shadow(0 0 2em #747bff);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafb);
filter: drop-shadow(0 0 2em #61dafb);
}
:root {
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
font-size: 16px;
line-height: 24px;
font-weight: 400;
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
font-size: 16px;
line-height: 24px;
font-weight: 400;
color: #0f0f0f;
background-color: #f6f6f6;
color: #0f0f0f;
background-color: #f6f6f6;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
.container {
margin: 0;
padding-top: 10vh;
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
margin: 0;
padding-top: 10vh;
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: 0.75s;
height: 6em;
padding: 1.5em;
will-change: filter;
transition: 0.75s;
}
.logo.tauri:hover {
filter: drop-shadow(0 0 2em #24c8db);
filter: drop-shadow(0 0 2em #24c8db);
}
.row {
display: flex;
justify-content: center;
display: flex;
justify-content: center;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
color: #535bf2;
}
h1 {
text-align: center;
text-align: center;
}
input,
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
color: #0f0f0f;
background-color: #ffffff;
transition: border-color 0.25s;
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
color: #0f0f0f;
background-color: #ffffff;
transition: border-color 0.25s;
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
}
button {
cursor: pointer;
cursor: pointer;
}
button:hover {
border-color: #396cd8;
border-color: #396cd8;
}
button:active {
border-color: #396cd8;
background-color: #e8e8e8;
border-color: #396cd8;
background-color: #e8e8e8;
}
input,
button {
outline: none;
outline: none;
}
#greet-input {
margin-right: 5px;
margin-right: 5px;
}
@media (prefers-color-scheme: dark) {
:root {
color: #f6f6f6;
background-color: #2f2f2f;
}
:root {
color: #f6f6f6;
background-color: #2f2f2f;
}
a:hover {
color: #24c8db;
}
a:hover {
color: #24c8db;
}
input,
button {
color: #ffffff;
background-color: #0f0f0f98;
}
button:active {
background-color: #0f0f0f69;
}
input,
button {
color: #ffffff;
background-color: #0f0f0f98;
}
button:active {
background-color: #0f0f0f69;
}
}

View File

@@ -54,11 +54,16 @@ function App() {
}
return (
<main className="container">
<h1>AI Code Assistant</h1>
<main
className="container"
style={{ height: "100vh", padding: 0, maxWidth: "100%" }}
>
{!projectPath ? (
<div className="selection-screen">
<div
className="selection-screen"
style={{ padding: "2rem", maxWidth: "800px", margin: "0 auto" }}
>
<h1>AI Code Assistant</h1>
<p>
Please select a project folder to start the Story-Driven Spec
Workflow.
@@ -66,31 +71,8 @@ function App() {
<button onClick={selectProject}>Open Project Directory</button>
</div>
) : (
<div className="workspace">
<div
className="toolbar"
style={{
padding: "10px",
background: "#f0f0f0",
borderRadius: "4px",
color: "#333",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<span>
<strong>Active Project:</strong> {projectPath}
</span>
<button
onClick={closeProject}
style={{ padding: "5px 10px", fontSize: "0.9em" }}
>
Close
</button>
</div>
<hr style={{ margin: "20px 0" }} />
<Chat />
<div className="workspace" style={{ height: "100%" }}>
<Chat projectPath={projectPath} onCloseProject={closeProject} />
</div>
)}

View File

@@ -4,7 +4,12 @@ import { listen } from "@tauri-apps/api/event";
import Markdown from "react-markdown";
import { Message, ProviderConfig } from "../types";
export function Chat() {
interface ChatProps {
projectPath: string;
onCloseProject: () => void;
}
export function Chat({ projectPath, onCloseProject }: ChatProps) {
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState("");
const [loading, setLoading] = useState(false);
@@ -86,56 +91,136 @@ export function Chat() {
display: "flex",
flexDirection: "column",
height: "100%",
maxWidth: "800px",
margin: "0 auto",
backgroundColor: "#171717",
color: "#ececec",
}}
>
{/* Settings Bar */}
{/* Sticky Header */}
<div
style={{
padding: "10px",
borderBottom: "1px solid #ddd",
padding: "12px 24px",
borderBottom: "1px solid #333",
display: "flex",
gap: "10px",
alignItems: "center",
justifyContent: "space-between",
background: "#171717",
flexShrink: 0,
fontSize: "0.9rem",
color: "#ececec",
}}
>
<label>Ollama Model:</label>
{availableModels.length > 0 ? (
<select
value={model}
onChange={(e) => setModel(e.target.value)}
style={{ padding: "5px" }}
>
{availableModels.map((m) => (
<option key={m} value={m}>
{m}
</option>
))}
</select>
) : (
<input
value={model}
onChange={(e) => setModel(e.target.value)}
placeholder="e.g. llama3, mistral"
style={{ padding: "5px" }}
/>
)}
<label
{/* Project Info */}
<div
style={{
display: "flex",
alignItems: "center",
gap: "5px",
marginLeft: "10px",
gap: "12px",
overflow: "hidden",
flex: 1,
marginRight: "20px",
}}
>
<input
type="checkbox"
checked={enableTools}
onChange={(e) => setEnableTools(e.target.checked)}
/>
Enable Tools
</label>
<div
title={projectPath}
style={{
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
fontWeight: "500",
color: "#aaa",
direction: "rtl",
textAlign: "left",
fontFamily: "monospace",
fontSize: "0.85em",
}}
>
{projectPath}
</div>
<button
onClick={onCloseProject}
style={{
background: "transparent",
border: "none",
cursor: "pointer",
color: "#999",
fontSize: "0.8em",
padding: "4px 8px",
borderRadius: "4px",
}}
onMouseOver={(e) => (e.currentTarget.style.background = "#333")}
onMouseOut={(e) =>
(e.currentTarget.style.background = "transparent")
}
>
</button>
</div>
{/* Model Controls */}
<div style={{ display: "flex", alignItems: "center", gap: "16px" }}>
{availableModels.length > 0 ? (
<select
value={model}
onChange={(e) => setModel(e.target.value)}
style={{
padding: "6px 32px 6px 16px",
borderRadius: "99px",
border: "none",
fontSize: "0.9em",
backgroundColor: "#2f2f2f",
color: "#ececec",
cursor: "pointer",
outline: "none",
appearance: "none",
WebkitAppearance: "none",
backgroundImage: `url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23ececec%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E")`,
backgroundRepeat: "no-repeat",
backgroundPosition: "right 12px center",
backgroundSize: "10px",
}}
>
{availableModels.map((m) => (
<option key={m} value={m}>
{m}
</option>
))}
</select>
) : (
<input
value={model}
onChange={(e) => setModel(e.target.value)}
placeholder="Model"
style={{
padding: "6px 12px",
borderRadius: "99px",
border: "none",
fontSize: "0.9em",
background: "#2f2f2f",
color: "#ececec",
outline: "none",
}}
/>
)}
<label
style={{
display: "flex",
alignItems: "center",
gap: "6px",
cursor: "pointer",
fontSize: "0.9em",
color: "#aaa",
}}
title="Allow the Agent to read/write files"
>
<input
type="checkbox"
checked={enableTools}
onChange={(e) => setEnableTools(e.target.checked)}
style={{ accentColor: "#000" }}
/>
<span>Tools</span>
</label>
</div>
</div>
{/* Messages Area */}
@@ -143,109 +228,190 @@ export function Chat() {
style={{
flex: 1,
overflowY: "auto",
padding: "20px",
padding: "20px 0",
display: "flex",
flexDirection: "column",
gap: "15px",
gap: "24px",
}}
>
{messages.map((msg, idx) => (
<div
key={idx}
className={`message ${msg.role}`}
style={{
alignSelf: msg.role === "user" ? "flex-end" : "flex-start",
maxWidth: "80%",
padding: "10px 15px",
borderRadius: "10px",
background:
msg.role === "user"
? "#007AFF"
: msg.role === "tool"
? "#f0f0f0"
: "#E5E5EA",
color: msg.role === "user" ? "white" : "black",
border: msg.role === "tool" ? "1px solid #ccc" : "none",
fontFamily: msg.role === "tool" ? "monospace" : "inherit",
fontSize: msg.role === "tool" ? "0.9em" : "1em",
whiteSpace: msg.role === "tool" ? "pre-wrap" : "normal",
}}
>
<strong>
{msg.role === "user"
? "You"
: msg.role === "tool"
? "Tool Output"
: "Agent"}
</strong>
{msg.role === "tool" ? (
<div style={{ maxHeight: "200px", overflow: "auto" }}>
{msg.content}
</div>
) : (
<Markdown>{msg.content}</Markdown>
)}
{/* Show Tool Calls if present */}
{msg.tool_calls && (
<div
style={{
maxWidth: "768px",
margin: "0 auto",
width: "100%",
padding: "0 24px",
display: "flex",
flexDirection: "column",
gap: "24px",
}}
>
{messages.map((msg, idx) => (
<div
key={idx}
style={{
display: "flex",
flexDirection: "column",
alignItems: msg.role === "user" ? "flex-end" : "flex-start",
}}
>
<div
style={{ marginTop: "10px", fontSize: "0.85em", color: "#666" }}
style={{
maxWidth: "100%",
padding: msg.role === "user" ? "10px 16px" : "0",
borderRadius: msg.role === "user" ? "20px" : "0",
background:
msg.role === "user"
? "#2f2f2f"
: msg.role === "tool"
? "#222"
: "transparent",
color: "#ececec",
border: msg.role === "tool" ? "1px solid #333" : "none",
fontFamily: msg.role === "tool" ? "monospace" : "inherit",
fontSize: msg.role === "tool" ? "0.85em" : "1em",
fontWeight: "500",
whiteSpace: msg.role === "tool" ? "pre-wrap" : "normal",
lineHeight: "1.6",
}}
>
{msg.tool_calls.map((tc, i) => (
{msg.role === "user" ? (
msg.content
) : msg.role === "tool" ? (
<div>
<strong
style={{
display: "block",
marginBottom: "4px",
color: "#aaa",
}}
>
Tool Output
</strong>
<div style={{ maxHeight: "300px", overflow: "auto" }}>
{msg.content}
</div>
</div>
) : (
<div className="markdown-body">
{/* Assuming global CSS handles standard markdown styling now */}
<Markdown>{msg.content}</Markdown>
</div>
)}
{/* Show Tool Calls if present */}
{msg.tool_calls && (
<div
key={i}
style={{
background: "rgba(0,0,0,0.05)",
padding: "5px",
borderRadius: "4px",
marginTop: "12px",
fontSize: "0.85em",
color: "#aaa",
display: "flex",
flexDirection: "column",
gap: "8px",
}}
>
🛠{" "}
<code>
{tc.function.name}({tc.function.arguments})
</code>
{msg.tool_calls.map((tc, i) => (
<div
key={i}
style={{
display: "flex",
alignItems: "center",
gap: "8px",
fontFamily: "monospace",
}}
>
<span style={{ color: "#888" }}>Running:</span>
<span
style={{
background: "#333",
padding: "2px 6px",
borderRadius: "4px",
}}
>
{tc.function.name}
</span>
</div>
))}
</div>
))}
)}
</div>
)}
</div>
))}
{loading && (
<div style={{ alignSelf: "flex-start", color: "#888" }}>
Thinking...
</div>
)}
<div ref={messagesEndRef} />
</div>
))}
{loading && (
<div
style={{
alignSelf: "flex-start",
color: "#888",
fontSize: "0.9em",
marginTop: "10px",
}}
>
<span className="pulse">Thinking...</span>
</div>
)}
<div ref={messagesEndRef} />
</div>
</div>
{/* Input Area */}
<div
style={{
padding: "20px",
borderTop: "1px solid #ddd",
padding: "24px",
background: "#171717",
display: "flex",
gap: "10px",
justifyContent: "center",
}}
>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && sendMessage()}
placeholder="Ask the agent to do something..."
<div
style={{
flex: 1,
padding: "10px",
borderRadius: "4px",
border: "1px solid #ccc",
maxWidth: "768px",
width: "100%",
position: "relative",
}}
/>
<button
onClick={sendMessage}
disabled={loading}
style={{ padding: "10px 20px" }}
>
Send
</button>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && sendMessage()}
placeholder="Send a message..."
style={{
width: "100%",
padding: "14px 20px",
paddingRight: "50px", // space for button
borderRadius: "24px",
border: "1px solid #333",
outline: "none",
fontSize: "1rem",
fontWeight: "500",
background: "#2f2f2f",
color: "#ececec",
boxShadow: "0 2px 6px rgba(0,0,0,0.02)",
}}
/>
<button
onClick={sendMessage}
disabled={loading}
style={{
position: "absolute",
right: "8px",
top: "50%",
transform: "translateY(-50%)",
background: "#ececec",
color: "black",
border: "none",
borderRadius: "50%",
width: "32px",
height: "32px",
display: "flex",
alignItems: "center",
justifyContent: "center",
cursor: loading ? "not-allowed" : "pointer",
opacity: loading ? 0.5 : 1,
}}
>
</button>
</div>
</div>
</div>
);