Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 97 additions & 8 deletions packages/frontend/src/app.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { LanguageModelV1Prompt } from "ai"
import { createEffect, For, onCleanup } from "solid-js"
import { createEffect, For, onCleanup, Show, createSignal } from "solid-js"
import { createStore } from "solid-js/store"
import SYSTEM_PROMPT from "./system.txt?raw"
import { hc } from "hono/client"
Expand All @@ -13,12 +13,19 @@ const providerMetadata = {
},
},
}

const systemPrompt = {
get: () => {
return localStorage.getItem("opencontrol:systemPrompt") ?? SYSTEM_PROMPT
},
set: (value: string) => {
localStorage.setItem("opencontrol:systemPrompt", value)
},
}
// Define initial system messages once
const getInitialPrompt = (): LanguageModelV1Prompt => [
{
role: "system",
content: SYSTEM_PROMPT,
content: systemPrompt.get(),
providerMetadata: {
anthropic: {
cacheControl: {
Expand Down Expand Up @@ -215,6 +222,44 @@ export function App() {
textarea?.focus()
}

const [showSystemPromptModal, setShowSystemPromptModal] = createSignal(false)
const [systemPromptValue, setSystemPromptValue] = createSignal(
systemPrompt.get(),
)

const modifySystemPrompt = () => {
setSystemPromptValue(systemPrompt.get())
setShowSystemPromptModal(true)
}

const handleSystemPromptSubmit = () => {
const newPrompt = systemPromptValue()
if (newPrompt.trim() !== "") {
systemPrompt.set(newPrompt)
setStore("prompt", getInitialPrompt())
}
setShowSystemPromptModal(false)
}

createEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (showSystemPromptModal()) {
if (e.key === "Escape") {
e.preventDefault()
setShowSystemPromptModal(false)
} else if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
e.preventDefault()
handleSystemPromptSubmit()
}
}
}

window.addEventListener("keydown", handleKeyDown)
onCleanup(() => {
window.removeEventListener("keydown", handleKeyDown)
})
})

return (
<div data-component="root" ref={root}>
<div data-component="messages">
Expand Down Expand Up @@ -292,13 +337,16 @@ export function App() {
<div data-slot="spacer"></div>
</div>
<div data-component="footer">
{store.prompt.length > 2 && !store.isProcessing && (
<div data-slot="clear">
<button data-component="clear-button" onClick={clearConversation}>
<div data-slot="footer-actions">
<button data-component="footer-action" onClick={modifySystemPrompt}>
🔧
</button>
{store.prompt.length > 2 && !store.isProcessing && (
<button data-component="footer-action" onClick={clearConversation}>
Clear
</button>
</div>
)}
)}
</div>
<div data-slot="chat">
<textarea
autofocus
Expand All @@ -320,6 +368,47 @@ export function App() {
/>
</div>
</div>

<Show when={showSystemPromptModal()}>
<div
data-component="dialog-overlay"
onClick={() => setShowSystemPromptModal(false)}
></div>
<div data-component="dialog-center">
<div data-slot="content" data-size="md">
<div data-slot="header">
<label data-size="md" data-slot="title" data-component="label">
🔧 System Prompt
</label>
</div>
<div data-slot="main">
<textarea
data-component="input"
style={{ "min-height": "200px", "margin-top": "10px" }}
value={systemPromptValue()}
onInput={(e) => setSystemPromptValue(e.currentTarget.value)}
/>
</div>
<div data-slot="footer">
<small data-component="footer-hint">
Press Esc to cancel, Ctrl+Enter to save
</small>
<button
data-component="footer-action"
onClick={() => setShowSystemPromptModal(false)}
>
Cancel
</button>
<button
data-component="footer-action"
onClick={handleSystemPromptSubmit}
>
Save
</button>
</div>
</div>
</div>
</Show>
</div>
)
}
71 changes: 65 additions & 6 deletions packages/frontend/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -196,13 +196,14 @@ body {
padding: 0 16px 16px;
box-sizing: border-box;

[data-slot="clear"] {
[data-slot="footer-actions"] {
position: relative;
width: 100%;
max-width: 780px;
display: flex;
justify-content: flex-end;
margin-bottom: 8px;
gap: 8px;
}

[data-slot="chat"] {
Expand All @@ -222,7 +223,7 @@ body {
}
}

[data-component="clear-button"] {
[data-component="footer-action"] {
background: var(--color-bravo200);
color: white;
border: none;
Expand All @@ -246,8 +247,8 @@ body {
@media (max-width: 600px) {
[data-component="footer"] {
padding-bottom: 8px;
[data-slot="clear"] {

[data-slot="footer-actions"] {
margin-bottom: 4px;
}

Expand All @@ -256,8 +257,8 @@ body {
border-radius: 8px;
}
}
[data-component="clear-button"] {

[data-component="footer-action"] {
padding: 4px 10px;
font-size: 12px;
}
Expand Down Expand Up @@ -330,3 +331,61 @@ body {
0% { transform: rotate(0deg); }
100% { transform: rotate(-720deg); }
}

/* Dialog Components */
[data-component="dialog-overlay"] {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 10;
backdrop-filter: blur(2px);
}

[data-component="dialog-center"] {
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
z-index: 20;

[data-slot="content"] {
background: var(--color-alpha200);
border-radius: 8px;
width: 90%;
max-width: 600px;
display: flex;
flex-direction: column;
max-height: 90vh;
overflow: hidden;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
border: 1px solid var(--color-bravo200);
}

[data-slot="content"] [data-slot="header"] {
padding: 16px;
border-bottom: 1px solid var(--color-bravo100);
}

[data-slot="content"] [data-slot="main"] {
padding: 16px;
overflow-y: auto;
flex: 1;
}

[data-slot="content"] [data-slot="footer"] {
padding: 16px;
display: flex;
justify-content: flex-end;
gap: 10px;
border-top: 1px solid var(--color-bravo100);
}

[data-component="footer-hint"] {
margin-right: auto;
opacity: 0.7;
display: flex;
align-items: center;
}
}