mirror of
https://github.com/farcasclaudiu/openclaw.git
synced 2026-06-27 15:01:15 +03:00
228 lines
7.6 KiB
TypeScript
228 lines
7.6 KiB
TypeScript
import { html } from "lit";
|
|
import { repeat } from "lit/directives/repeat.js";
|
|
|
|
import type { AppViewState } from "./app-view-state";
|
|
import { iconForTab, pathForTab, titleForTab, type Tab } from "./navigation";
|
|
import { loadChatHistory } from "./controllers/chat";
|
|
import { syncUrlWithSessionKey } from "./app-settings";
|
|
import type { SessionsListResult } from "./types";
|
|
import type { ThemeMode } from "./theme";
|
|
import type { ThemeTransitionContext } from "./theme-transition";
|
|
|
|
export function renderTab(state: AppViewState, tab: Tab) {
|
|
const href = pathForTab(tab, state.basePath);
|
|
return html`
|
|
<a
|
|
href=${href}
|
|
class="nav-item ${state.tab === tab ? "active" : ""}"
|
|
@click=${(event: MouseEvent) => {
|
|
if (
|
|
event.defaultPrevented ||
|
|
event.button !== 0 ||
|
|
event.metaKey ||
|
|
event.ctrlKey ||
|
|
event.shiftKey ||
|
|
event.altKey
|
|
) {
|
|
return;
|
|
}
|
|
event.preventDefault();
|
|
state.setTab(tab);
|
|
}}
|
|
title=${titleForTab(tab)}
|
|
>
|
|
<span class="nav-item__icon" aria-hidden="true">${iconForTab(tab)}</span>
|
|
<span class="nav-item__text">${titleForTab(tab)}</span>
|
|
</a>
|
|
`;
|
|
}
|
|
|
|
export function renderChatControls(state: AppViewState) {
|
|
const sessionOptions = resolveSessionOptions(state.sessionKey, state.sessionsResult);
|
|
// Refresh icon
|
|
const refreshIcon = html`<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"></path><path d="M21 3v5h-5"></path></svg>`;
|
|
const focusIcon = html`<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7V4h3"></path><path d="M20 7V4h-3"></path><path d="M4 17v3h3"></path><path d="M20 17v3h-3"></path><circle cx="12" cy="12" r="3"></circle></svg>`;
|
|
return html`
|
|
<div class="chat-controls">
|
|
<label class="field chat-controls__session">
|
|
<select
|
|
.value=${state.sessionKey}
|
|
?disabled=${!state.connected}
|
|
@change=${(e: Event) => {
|
|
const next = (e.target as HTMLSelectElement).value;
|
|
state.sessionKey = next;
|
|
state.chatMessage = "";
|
|
state.chatStream = null;
|
|
state.chatStreamStartedAt = null;
|
|
state.chatRunId = null;
|
|
state.resetToolStream();
|
|
state.resetChatScroll();
|
|
state.applySettings({
|
|
...state.settings,
|
|
sessionKey: next,
|
|
lastActiveSessionKey: next,
|
|
});
|
|
void state.loadAssistantIdentity();
|
|
syncUrlWithSessionKey(state, next, true);
|
|
void loadChatHistory(state);
|
|
}}
|
|
>
|
|
${repeat(
|
|
sessionOptions,
|
|
(entry) => entry.key,
|
|
(entry) =>
|
|
html`<option value=${entry.key}>
|
|
${entry.displayName ?? entry.key}
|
|
</option>`,
|
|
)}
|
|
</select>
|
|
</label>
|
|
<button
|
|
class="btn btn--sm btn--icon"
|
|
?disabled=${state.chatLoading || !state.connected}
|
|
@click=${() => {
|
|
state.resetToolStream();
|
|
void loadChatHistory(state);
|
|
}}
|
|
title="Refresh chat history"
|
|
>
|
|
${refreshIcon}
|
|
</button>
|
|
<span class="chat-controls__separator">|</span>
|
|
<button
|
|
class="btn btn--sm btn--icon ${state.settings.chatShowThinking ? "active" : ""}"
|
|
@click=${() =>
|
|
state.applySettings({
|
|
...state.settings,
|
|
chatShowThinking: !state.settings.chatShowThinking,
|
|
})}
|
|
aria-pressed=${state.settings.chatShowThinking}
|
|
title="Toggle assistant thinking/working output"
|
|
>
|
|
🧠
|
|
</button>
|
|
<button
|
|
class="btn btn--sm btn--icon ${state.settings.chatFocusMode ? "active" : ""}"
|
|
@click=${() =>
|
|
state.applySettings({
|
|
...state.settings,
|
|
chatFocusMode: !state.settings.chatFocusMode,
|
|
})}
|
|
aria-pressed=${state.settings.chatFocusMode}
|
|
title="Toggle focus mode (hide sidebar + page header)"
|
|
>
|
|
${focusIcon}
|
|
</button>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function resolveSessionOptions(sessionKey: string, sessions: SessionsListResult | null) {
|
|
const seen = new Set<string>();
|
|
const options: Array<{ key: string; displayName?: string }> = [];
|
|
|
|
const resolvedCurrent = sessions?.sessions?.find((s) => s.key === sessionKey);
|
|
|
|
// Add current session key first
|
|
seen.add(sessionKey);
|
|
options.push({ key: sessionKey, displayName: resolvedCurrent?.displayName });
|
|
|
|
// Add sessions from the result
|
|
if (sessions?.sessions) {
|
|
for (const s of sessions.sessions) {
|
|
if (!seen.has(s.key)) {
|
|
seen.add(s.key);
|
|
options.push({ key: s.key, displayName: s.displayName });
|
|
}
|
|
}
|
|
}
|
|
|
|
return options;
|
|
}
|
|
|
|
const THEME_ORDER: ThemeMode[] = ["system", "light", "dark"];
|
|
|
|
export function renderThemeToggle(state: AppViewState) {
|
|
const index = Math.max(0, THEME_ORDER.indexOf(state.theme));
|
|
const applyTheme = (next: ThemeMode) => (event: MouseEvent) => {
|
|
const element = event.currentTarget as HTMLElement;
|
|
const context: ThemeTransitionContext = { element };
|
|
if (event.clientX || event.clientY) {
|
|
context.pointerClientX = event.clientX;
|
|
context.pointerClientY = event.clientY;
|
|
}
|
|
state.setTheme(next, context);
|
|
};
|
|
|
|
return html`
|
|
<div class="theme-toggle" style="--theme-index: ${index};">
|
|
<div class="theme-toggle__track" role="group" aria-label="Theme">
|
|
<span class="theme-toggle__indicator"></span>
|
|
<button
|
|
class="theme-toggle__button ${state.theme === "system" ? "active" : ""}"
|
|
@click=${applyTheme("system")}
|
|
aria-pressed=${state.theme === "system"}
|
|
aria-label="System theme"
|
|
title="System"
|
|
>
|
|
${renderMonitorIcon()}
|
|
</button>
|
|
<button
|
|
class="theme-toggle__button ${state.theme === "light" ? "active" : ""}"
|
|
@click=${applyTheme("light")}
|
|
aria-pressed=${state.theme === "light"}
|
|
aria-label="Light theme"
|
|
title="Light"
|
|
>
|
|
${renderSunIcon()}
|
|
</button>
|
|
<button
|
|
class="theme-toggle__button ${state.theme === "dark" ? "active" : ""}"
|
|
@click=${applyTheme("dark")}
|
|
aria-pressed=${state.theme === "dark"}
|
|
aria-label="Dark theme"
|
|
title="Dark"
|
|
>
|
|
${renderMoonIcon()}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function renderSunIcon() {
|
|
return html`
|
|
<svg class="theme-icon" viewBox="0 0 24 24" aria-hidden="true">
|
|
<circle cx="12" cy="12" r="4"></circle>
|
|
<path d="M12 2v2"></path>
|
|
<path d="M12 20v2"></path>
|
|
<path d="m4.93 4.93 1.41 1.41"></path>
|
|
<path d="m17.66 17.66 1.41 1.41"></path>
|
|
<path d="M2 12h2"></path>
|
|
<path d="M20 12h2"></path>
|
|
<path d="m6.34 17.66-1.41 1.41"></path>
|
|
<path d="m19.07 4.93-1.41 1.41"></path>
|
|
</svg>
|
|
`;
|
|
}
|
|
|
|
function renderMoonIcon() {
|
|
return html`
|
|
<svg class="theme-icon" viewBox="0 0 24 24" aria-hidden="true">
|
|
<path
|
|
d="M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401"
|
|
></path>
|
|
</svg>
|
|
`;
|
|
}
|
|
|
|
function renderMonitorIcon() {
|
|
return html`
|
|
<svg class="theme-icon" viewBox="0 0 24 24" aria-hidden="true">
|
|
<rect width="20" height="14" x="2" y="3" rx="2"></rect>
|
|
<line x1="8" x2="16" y1="21" y2="21"></line>
|
|
<line x1="12" x2="12" y1="17" y2="21"></line>
|
|
</svg>
|
|
`;
|
|
}
|