Major rebranding, i18n, dashboard, shared folders, role capabilities
- Rebrand: new app/agent icons, SKALD.md, warm "paper" CSS palette (terracotta accent, --radius tokens, WCAG contrast, reduced-motion), updated favicon, tray icon, skaldkonur asset - i18n: backend crate (i18n.rs, locale column, ui_locale config), frontend library (web/lib/i18n.js, I18nMixin, t(key)), translation files (web/i18n/), every component wired - Dashboard: <dashboard-page> replaces old home-page content; <app-copilot> becomes the landing page (full/dock layout modes) - Shared folders: API endpoints (shared_folders.rs), frontend page, can_write membership, container mount topology, user_fs routing - Role capabilities: new db table & authorization seam (data not enums), roles.attrs JSON for ui_mode / interface select - Setup: skald-setup prompts for language + password, sets ui_locale - General: components migrated to CSS variables, Lit conventions cleanup, connectors/catalog/marketplace/approval refactoring
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { html, nothing } from 'lit';
|
||||
import { ChatSession } from '../../lib/chat-session.js';
|
||||
import { t } from '../../lib/i18n.js';
|
||||
import { renderMsg, renderAttachmentChips } from '../copilot-render.js';
|
||||
|
||||
export class ChatPage extends ChatSession {
|
||||
@@ -105,17 +106,17 @@ export class ChatPage extends ChatSession {
|
||||
<div class="mobile-section-header">
|
||||
<span class="mobile-section-title">
|
||||
${this._inProject ? html`
|
||||
<button class="chat-page-back" title="Back to General"
|
||||
<button class="chat-page-back" title=${t('chat.mobile.back_general')}
|
||||
@click=${() => this._exitProject()}>
|
||||
<i class="bi bi-chevron-left"></i>
|
||||
</button>
|
||||
<i class="bi bi-folder2-open"></i> ${this.label || 'Project'}
|
||||
` : html`<i class="bi bi-chat-dots-fill"></i> Chat`}
|
||||
<i class="bi bi-folder2-open"></i> ${this.label || t('chat.mobile.project')}
|
||||
` : html`<i class="bi bi-chat-dots-fill"></i> ${t('chat.mobile.chat')}`}
|
||||
</span>
|
||||
<div class="chat-page-header-actions">
|
||||
<button
|
||||
class="btn btn-sm btn-outline-secondary"
|
||||
title="New conversation"
|
||||
title=${t('chat.new_session')}
|
||||
@click=${() => this._startNewSession()}
|
||||
><i class="bi bi-trash"></i></button>
|
||||
</div>
|
||||
@@ -125,14 +126,14 @@ export class ChatPage extends ChatSession {
|
||||
${this._messages.length === 0 ? html`
|
||||
<div class="chat-page-empty">
|
||||
<i class="bi bi-stars"></i>
|
||||
<p>Ask me anything</p>
|
||||
<p>${t('chat.mobile.ask')}</p>
|
||||
</div>
|
||||
` : this._messages.map(m => renderMsg(this, m))}
|
||||
|
||||
${this._waiting ? html`
|
||||
<div class="copilot-msg assistant copilot-thinking">
|
||||
<span class="spinner-border spinner-border-sm me-2" role="status"></span>
|
||||
Thinking…
|
||||
${t('chat.thinking')}
|
||||
</div>
|
||||
` : nothing}
|
||||
</div>
|
||||
@@ -152,7 +153,7 @@ export class ChatPage extends ChatSession {
|
||||
<textarea
|
||||
class="chat-page-textarea"
|
||||
rows="1"
|
||||
placeholder="Type a message…"
|
||||
placeholder=${t('chat.mobile.placeholder')}
|
||||
@input=${(e) => this._autoResize(e.target)}
|
||||
@paste=${(e) => this._onPaste(e)}
|
||||
></textarea>
|
||||
@@ -160,7 +161,7 @@ export class ChatPage extends ChatSession {
|
||||
<div class="chat-page-toolbar-left">
|
||||
<button
|
||||
class="btn btn-sm btn-outline-secondary chat-page-attach-btn"
|
||||
title="Attach files"
|
||||
title=${t('chat.attach')}
|
||||
@click=${() => this.querySelector('.chat-page-file-input')?.click()}
|
||||
><i class="bi bi-paperclip"></i></button>
|
||||
${this._providers.length > 1 ? html`
|
||||
@@ -179,7 +180,7 @@ export class ChatPage extends ChatSession {
|
||||
${this._hasTranscribe ? html`
|
||||
<button
|
||||
class="chat-page-mic-btn ${this._recording ? 'chat-page-mic-btn--recording' : ''}"
|
||||
title="${this._recording ? 'Stop recording' : 'Record voice'}"
|
||||
title=${this._recording ? t('chat.mobile.stop_record') : t('chat.mobile.record_voice')}
|
||||
@click=${() => this._toggleRecording()}
|
||||
>
|
||||
<i class="bi ${this._recording ? 'bi-stop-circle-fill' : 'bi-mic-fill'}"></i>
|
||||
@@ -189,13 +190,13 @@ export class ChatPage extends ChatSession {
|
||||
? html`<button
|
||||
class="chat-page-send chat-page-send--stop"
|
||||
@click=${() => this._cancel()}
|
||||
title="Stop"
|
||||
title=${t('chat.stop')}
|
||||
><i class="bi bi-stop-fill"></i></button>`
|
||||
: nothing}
|
||||
<button
|
||||
class="chat-page-send"
|
||||
@click=${() => this._send()}
|
||||
title="Send"
|
||||
title=${t('chat.send')}
|
||||
><i class="bi bi-send-fill"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { t } from '../../lib/i18n.js';
|
||||
|
||||
// Shared vocabulary for the Connectors list and a connector's own page.
|
||||
//
|
||||
// Both surfaces have to answer "what state is this connector in?" and both draw the
|
||||
@@ -13,14 +15,25 @@ export function connectorIconUrl(name, size = 'sm') {
|
||||
/// How each status reads on a chip. `tone` maps to the `connector-chip--*` accents
|
||||
/// in `web/css/connectors.css`.
|
||||
export const STATUS_LABEL = {
|
||||
active: { text: 'active', tone: 'ok' },
|
||||
pending: { text: 'needs fix', tone: 'script' },
|
||||
needs_login: { text: 'needs sign-in', tone: 'script' },
|
||||
enabled: { text: 'enabled', tone: 'scope' },
|
||||
off: { text: 'off', tone: '' },
|
||||
available: { text: 'available', tone: '' },
|
||||
active: { tone: 'ok' },
|
||||
pending: { tone: 'script' },
|
||||
needs_login: { tone: 'script' },
|
||||
enabled: { tone: 'scope' },
|
||||
off: { tone: '' },
|
||||
available: { tone: '' },
|
||||
};
|
||||
|
||||
export function statusText(status) {
|
||||
return {
|
||||
active: t('connectors.status.active'),
|
||||
pending: t('connectors.status.needs_fix'),
|
||||
needs_login: t('connectors.status.needs_signin'),
|
||||
enabled: t('connectors.status.enabled'),
|
||||
off: t('connectors.status.off'),
|
||||
available: t('connectors.status.available'),
|
||||
}[status] ?? status;
|
||||
}
|
||||
|
||||
/// The one place that decides what a connector's state *is*, from whichever runtime
|
||||
/// rows exist for it.
|
||||
///
|
||||
|
||||
@@ -2,6 +2,7 @@ import { html, nothing } from 'lit';
|
||||
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
|
||||
import { LightElement, renderMarkdown } from '../../lib/base.js';
|
||||
import { fileWatcher } from '../../lib/file-watcher.js';
|
||||
import { t } from '../../lib/i18n.js';
|
||||
|
||||
/**
|
||||
* Shared file-viewer engine. Holds all of the fetch / kind-detection /
|
||||
@@ -346,7 +347,7 @@ export class FileViewerBase extends LightElement {
|
||||
const showingSource = this._htmlMode === 'source';
|
||||
return html`<button
|
||||
class=${btnClass}
|
||||
title=${showingSource ? 'Show preview' : 'Show source'}
|
||||
title=${showingSource ? t('fv.mode_preview') : t('fv.mode_source')}
|
||||
@click=${() => this._toggleHtmlMode()}>
|
||||
<i class="bi ${showingSource ? 'bi-eye' : 'bi-code-slash'}"></i>
|
||||
</button>`;
|
||||
@@ -389,7 +390,7 @@ export class FileViewerBase extends LightElement {
|
||||
if (this._kind === 'binary') {
|
||||
return html`<div class="fv-state text-muted">
|
||||
<i class="bi bi-file-earmark-binary fs-3 d-block mb-2"></i>
|
||||
Preview not available for this file type.
|
||||
${t('fv.binary_unavailable')}
|
||||
</div>`;
|
||||
}
|
||||
if (this._kind === 'html') {
|
||||
@@ -417,7 +418,7 @@ export class FileViewerBase extends LightElement {
|
||||
return html`
|
||||
${this._compileError
|
||||
? html`<details class="fv-compile-error">
|
||||
<summary><i class="bi bi-exclamation-triangle text-warning"></i> LaTeX compilation failed — showing source instead</summary>
|
||||
<summary><i class="bi bi-exclamation-triangle text-warning"></i> ${t('fv.latex_failed')}</summary>
|
||||
<pre>${this._compileError}</pre>
|
||||
</details>`
|
||||
: nothing}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { html, nothing } from 'lit';
|
||||
import { t } from '../../lib/i18n.js';
|
||||
import { FileViewerBase } from './file-viewer-base.js';
|
||||
|
||||
/**
|
||||
@@ -43,14 +44,14 @@ export class MobileFileViewerPage extends FileViewerBase {
|
||||
<div class="mobile-file-viewer">
|
||||
<div class="mobile-section-header">
|
||||
<span class="mobile-section-title">
|
||||
<button class="chat-page-back" title="Back" @click=${() => this._back()}>
|
||||
<button class="chat-page-back" title=${t('fv.back')} @click=${() => this._back()}>
|
||||
<i class="bi bi-arrow-left"></i>
|
||||
</button>
|
||||
<span class="fv-mobile-name" title=${this.path ?? ''}><bdi>${this._basename()}</bdi></span>
|
||||
</span>
|
||||
<span class="fv-header-actions">
|
||||
${this._renderModeToggle('chat-page-back')}
|
||||
<button class="chat-page-back" title="Download" @click=${() => this._download()}>
|
||||
<button class="chat-page-back" title=${t('fv.download')} @click=${() => this._download()}>
|
||||
<i class="bi bi-download"></i>
|
||||
</button>
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user