Changeset 0.25.1 (#161)
This commit is contained in:
@@ -393,50 +393,101 @@ Vanilla JavaScript, no framework. The frontend ships as static files served by n
|
|||||||
|
|
||||||
```
|
```
|
||||||
src/
|
src/
|
||||||
├── index.html # Single-page app shell
|
├── index.html # Redirect stub → server-rendered surfaces
|
||||||
├── sw.js # Service worker (offline cache)
|
├── sw.js # Service worker (offline cache, BUILD_HASH)
|
||||||
├── manifest.json # PWA manifest
|
├── manifest.json # PWA manifest
|
||||||
├── css/
|
├── css/
|
||||||
│ ├── styles.css # Core styles (CSS variables, light/dark themes)
|
│ ├── variables.css # Theme vars, reset, base styles
|
||||||
│ └── memory.css # Memory settings + admin review panel styles
|
│ ├── layout.css # App shell, workspace, sidebar, responsive
|
||||||
|
│ ├── primitives.css # Buttons, forms, toasts, toggles, badges, icon-btn
|
||||||
|
│ ├── modals.css # Modal system, debug, command palette, confirm
|
||||||
|
│ ├── chat.css # Chat area, markdown, input, files, lightbox
|
||||||
|
│ ├── panels.css # Side panel, preview, notes, graph
|
||||||
|
│ ├── surfaces.css # Admin, settings, editor, projects, notes layouts
|
||||||
|
│ ├── splash.css # Auth splash, PWA banner
|
||||||
|
│ ├── chat-pane.css # ChatPane component styles
|
||||||
|
│ ├── pane-container.css # PaneContainer resize/tab styles
|
||||||
|
│ ├── user-menu.css # UserMenu flyout styles
|
||||||
|
│ ├── editor-surface.css # Editor surface 3-pane layout
|
||||||
|
│ ├── channel-models.css # @mention autocomplete, model roster
|
||||||
|
│ ├── memory.css # Memory settings + admin review
|
||||||
|
│ ├── notifications.css # Notification dropdown + list
|
||||||
|
│ ├── notification-prefs.css # SMTP config, notification pref table
|
||||||
|
│ ├── persona-kb.css # Persona KB picker panel
|
||||||
|
│ ├── admin-surfaces.css # Admin surface management
|
||||||
|
│ └── tool-grants.css # Persona tool grants checkbox list
|
||||||
├── js/
|
├── js/
|
||||||
|
│ ├── ── Primitives (base.html, all surfaces) ──
|
||||||
|
│ ├── app-state.js # Global state (App, API refs)
|
||||||
│ ├── api.js # HTTP client with token refresh
|
│ ├── api.js # HTTP client with token refresh
|
||||||
│ ├── app.js # Application state machine, startup, routing
|
|
||||||
│ ├── chat.js # Chat input abstraction, message send/receive
|
|
||||||
│ ├── channel-models.js # @mention autocomplete, channel model roster
|
|
||||||
│ ├── events.js # Labeled event bus + WebSocket bridge
|
│ ├── events.js # Labeled event bus + WebSocket bridge
|
||||||
│ ├── debug.js # Debug panel, diagnostics, state export
|
│ ├── ui-primitives.js # esc(), componentMixin, Providers, Roles,
|
||||||
│ ├── ui-core.js # DOM rendering, sidebar, modals, panels
|
│ │ # renderCapBadges, renderProviderForm/List,
|
||||||
│ ├── ui-format.js # Markdown rendering, @mention pills, code blocks
|
│ │ # renderRoleConfig, renderUsageDashboard,
|
||||||
│ ├── ui-settings.js # Settings tabs, theme, appearance, teams
|
│ │ # showConfirm, showPrompt, openModal/closeModal,
|
||||||
│ ├── ui-admin.js # Admin panel rendering
|
│ │ # Theme, mountAvatarUpload
|
||||||
│ ├── admin-handlers.js # Admin CRUD operations + extension editors
|
│ ├── ui-format.js # Markdown rendering, code blocks, time formatting
|
||||||
|
│ ├── ui-core.js # DOM rendering, sidebar, navigation, toast
|
||||||
|
│ ├── pages.js # Page routing, splash, login
|
||||||
|
│ │
|
||||||
|
│ ├── ── Components (base.html, all surfaces) ──
|
||||||
|
│ ├── user-menu.js # UserMenu template+factory component
|
||||||
|
│ ├── model-selector.js # ModelSelector template+factory component
|
||||||
|
│ ├── file-tree.js # FileTree template+factory component
|
||||||
|
│ ├── code-editor.js # CodeEditor template+factory component (CM6)
|
||||||
|
│ ├── note-editor.js # NoteEditor template+factory component
|
||||||
|
│ ├── chat-pane.js # ChatPane template+factory component
|
||||||
|
│ ├── pane-container.js # PaneContainer (resizable multi-pane layout)
|
||||||
|
│ ├── debug.js # Debug panel + diagnostics
|
||||||
|
│ ├── repl.js # Browser REPL console
|
||||||
|
│ │
|
||||||
|
│ ├── ── Surface: Chat ──
|
||||||
|
│ ├── app.js # Chat surface boot, auth, WebSocket
|
||||||
|
│ ├── chat.js # Chat message send/receive, stream
|
||||||
|
│ ├── extensions.js # Extension framework + sandbox
|
||||||
|
│ ├── panels.js # Side panel registry
|
||||||
|
│ ├── ui-settings.js # Settings modal tabs (chat surface)
|
||||||
|
│ ├── ui-admin.js # Admin navigation + section routing
|
||||||
|
│ ├── admin-handlers.js # Admin CRUD + extension editors
|
||||||
|
│ ├── admin-scaffold.js # Admin section HTML scaffolding
|
||||||
│ ├── settings-handlers.js # User settings CRUD, command palette
|
│ ├── settings-handlers.js # User settings CRUD, command palette
|
||||||
│ ├── tokens.js # Input token counting + context budget
|
│ ├── tokens.js # Input token counting + context budget
|
||||||
│ ├── attachments.js # File upload, paste-to-file, lightbox
|
│ ├── files.js # File upload, paste-to-file, lightbox
|
||||||
│ ├── notes.js # Notes panel CRUD + graph + daily notes
|
│ ├── notes.js # Notes panel CRUD + graph + daily notes
|
||||||
│ ├── note-graph.js # Canvas force-directed graph visualization
|
│ ├── note-graph.js # Canvas force-directed graph visualization
|
||||||
│ ├── projects-ui.js # Project sidebar, context menu, drag-and-drop
|
│ ├── projects-ui.js # Project sidebar, context menu, DnD
|
||||||
│ ├── knowledge.js # Knowledge base UI
|
│ ├── channel-models.js # Channel model roster + @mention AC
|
||||||
│ ├── memory-ui.js # Memory settings tab + admin review panel
|
│ ├── tools-toggle.js # Tool enable/disable popup
|
||||||
|
│ ├── knowledge-ui.js # Knowledge base picker + management
|
||||||
|
│ ├── persona-kb.js # Persona KB assignment panel
|
||||||
|
│ ├── memory-ui.js # Memory settings + admin review
|
||||||
|
│ ├── notifications.js # Notification bell + dropdown
|
||||||
|
│ ├── notification-prefs.js # SMTP config + notification prefs
|
||||||
|
│ │
|
||||||
|
│ ├── ── Surface: Editor ──
|
||||||
|
│ ├── editor-surface.js # Editor 3-pane boot, workspace management
|
||||||
|
│ │
|
||||||
|
│ ├── ── Surface: Admin ──
|
||||||
|
│ ├── admin-surfaces.js # Surface management (admin only)
|
||||||
|
│ │
|
||||||
|
│ ├── ── Surface: Login ──
|
||||||
|
│ ├── pages-splash.js # Login/register splash page
|
||||||
|
│ │
|
||||||
│ └── __tests__/ # Node.js test suite (node --test)
|
│ └── __tests__/ # Node.js test suite (node --test)
|
||||||
├── editor/ # CM6 source (compiled at build time)
|
├── editor/ # CM6 source (compiled at build time)
|
||||||
│ ├── package.json # CM6 + language mode dependencies
|
│ ├── package.json # CM6 + language mode dependencies
|
||||||
│ ├── package-lock.json # Lockfile for npm ci
|
|
||||||
│ ├── build.mjs # esbuild script → IIFE bundle
|
│ ├── build.mjs # esbuild script → IIFE bundle
|
||||||
│ ├── index.mjs # Bundle entrypoint (window.CM)
|
│ ├── index.mjs # Bundle entrypoint (window.CM)
|
||||||
│ ├── chat-input.mjs # Chat input factory (markdown, code blocks)
|
│ ├── chat-input.mjs # Chat input factory (markdown, code blocks)
|
||||||
│ ├── code-editor.mjs # Code editor factory (syntax, keybindings)
|
│ ├── code-editor.mjs # Code editor factory (syntax, keybindings)
|
||||||
│ ├── note-editor.mjs # Note editor factory (markdown + wikilinks)
|
│ ├── note-editor.mjs # Note editor factory (markdown + wikilinks)
|
||||||
│ ├── wikilink.mjs # CM6 wikilink extension (parse, decorate, autocomplete)
|
│ ├── wikilink.mjs # CM6 wikilink extension
|
||||||
│ └── theme.mjs # Switchboard + chat input + note editor themes
|
│ └── theme.mjs # CM6 themes (dark/light)
|
||||||
└── vendor/ # Vendored libraries (local + CDN fallback)
|
└── vendor/ # Vendored libraries (loaded in base.html)
|
||||||
├── marked/ # Markdown renderer
|
├── marked.min.js # Markdown renderer
|
||||||
├── purify/ # DOMPurify (XSS protection)
|
├── purify.min.js # DOMPurify (XSS protection)
|
||||||
├── mermaid/ # Diagram renderer
|
└── codemirror/
|
||||||
├── katex/ # Math renderer
|
└── codemirror.bundle.js # CM6 bundle (~295KB min, ~90KB gzip)
|
||||||
└── codemirror/ # CM6 bundle (built by Docker)
|
|
||||||
└── codemirror.bundle.js # ~295KB min, ~90KB gzip
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### CM6 Integration
|
### CM6 Integration
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# DESIGN — Surface & Extension Architecture
|
# DESIGN — Surface & Extension Architecture
|
||||||
|
|
||||||
**Status:** Accepted
|
**Status:** Accepted (Phases 1–2 implemented in v0.25.1)
|
||||||
**Scope:** Primitives, Components, Surfaces, Extension hooks, Themes
|
**Scope:** Primitives, Components, Surfaces, Extension hooks, Themes
|
||||||
**Depends on:** v0.22.5 (Go template engine), v0.22.7 (ChatPane)
|
**Depends on:** v0.22.5 (Go template engine), v0.22.7 (ChatPane), v0.25.1 (audit)
|
||||||
**Informs:** v0.22.8+, EXTENSIONS.md rewrite, ARCHITECTURE.md update, ICD-API, ICD-SURFACE
|
**Informs:** v0.22.8+, EXTENSIONS.md rewrite, ARCHITECTURE.md update, ICD-API, ICD-SURFACE
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -159,7 +159,9 @@ Primitives.menu({ anchor: el, items: [...], onSelect: fn })
|
|||||||
Primitives.dialog({ title: 'Confirm', body: el, onConfirm: fn })
|
Primitives.dialog({ title: 'Confirm', body: el, onConfirm: fn })
|
||||||
```
|
```
|
||||||
|
|
||||||
Current locations: `ui-primitives.js`, `ui-primitives-additions.js`,
|
Current locations: `ui-primitives.js` (factory functions, `esc()`,
|
||||||
|
`createComponentRegistry()`, `componentMixin()`, `showConfirm()`,
|
||||||
|
`showPrompt()`, `Theme`, `mountAvatarUpload()`)
|
||||||
and Go template components (`model-select.html`, `team-select.html`,
|
and Go template components (`model-select.html`, `team-select.html`,
|
||||||
`file-upload.html`). These converge into one coherent set.
|
`file-upload.html`). These converge into one coherent set.
|
||||||
|
|
||||||
@@ -202,23 +204,42 @@ to interact with.
|
|||||||
|
|
||||||
### Instance Pattern
|
### Instance Pattern
|
||||||
|
|
||||||
Components use the factory pattern established by ChatPane:
|
Components use `createComponentRegistry()` and `componentMixin()` from
|
||||||
|
`ui-primitives.js` for shared lifecycle (instance tracking, event
|
||||||
|
listener cleanup, destroy):
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const pane = ChatPane.create({
|
const ChatPane = {
|
||||||
messagesEl: document.getElementById('editorChatMessages'),
|
...createComponentRegistry('ChatPane'),
|
||||||
inputEl: document.getElementById('editorChatInput'),
|
|
||||||
sendBtnEl: document.getElementById('editorSendBtn'),
|
|
||||||
channelId: workspaceChatId,
|
|
||||||
standalone: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Lifecycle
|
create(opts) {
|
||||||
pane.renderMessages(msgs);
|
const instance = componentMixin({
|
||||||
pane.streamResponse(resp, msgs);
|
id: opts.id || 'pane-' + Date.now(),
|
||||||
pane.destroy();
|
messagesEl: opts.messagesEl,
|
||||||
|
inputEl: opts.inputEl,
|
||||||
|
channelId: opts.channelId || null,
|
||||||
|
// ... domain-specific state
|
||||||
|
|
||||||
|
// Component-specific teardown (called by mixin's destroy())
|
||||||
|
_cleanup() {
|
||||||
|
if (this._cmView) { this._cmView.destroy(); this._cmView = null; }
|
||||||
|
},
|
||||||
|
}, ChatPane);
|
||||||
|
|
||||||
|
ChatPane._register(instance.id, instance);
|
||||||
|
return instance;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Custom queries beyond .get(id)
|
||||||
|
forChannel(channelId) { /* ... */ },
|
||||||
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The mixin provides `_on(el, event, handler)` for tracked event binding
|
||||||
|
and `destroy()` which calls `_cleanup()` then removes all listeners and
|
||||||
|
unregisters from the registry. Components that need extra teardown define
|
||||||
|
`_cleanup()`.
|
||||||
|
|
||||||
Components can be instantiated multiple times on the same page (editor
|
Components can be instantiated multiple times on the same page (editor
|
||||||
has a ChatPane, notes has a ChatPane — independent instances).
|
has a ChatPane, notes has a ChatPane — independent instances).
|
||||||
|
|
||||||
@@ -567,7 +588,7 @@ accommodates it without redesign.
|
|||||||
b. Register hooks (pre-completion, post-render, etc.)
|
b. Register hooks (pre-completion, post-render, etc.)
|
||||||
c. Register surface injections for current surface
|
c. Register surface injections for current surface
|
||||||
d. Initialize tool bridges
|
d. Initialize tool bridges
|
||||||
5. Surface-specific JS runs (app.js for chat, editor-mode.js, etc.)
|
5. Surface-specific JS runs (app.js for chat, editor-surface.js, etc.)
|
||||||
```
|
```
|
||||||
|
|
||||||
`Extensions.boot()` replaces the current chat-specific
|
`Extensions.boot()` replaces the current chat-specific
|
||||||
@@ -672,8 +693,8 @@ post-render hooks use to discover display content from any extension.
|
|||||||
|
|
||||||
| Current State | Target State |
|
| Current State | Target State |
|
||||||
|---------------|-------------|
|
|---------------|-------------|
|
||||||
| `ui-primitives.js` + `ui-primitives-additions.js` | Primitive catalog with stable factory API |
|
| `ui-primitives.js` (merged, with `esc()`, `createComponentRegistry()`, `componentMixin()`) | ✅ Done (v0.25.1) |
|
||||||
| `ChatPane.create()` (v0.22.7) | Component catalog with shared instance pattern |
|
| `ChatPane.create()` + 5 other components using shared mixin | ✅ Done (v0.25.1) |
|
||||||
| 5 Go template routes | Surface registry (hardcoded core, manifest-driven extensions) |
|
| 5 Go template routes | Surface registry (hardcoded core, manifest-driven extensions) |
|
||||||
| `ctx.ui.replace()` / `ctx.ui.inject()` | Hook 6: Surface Injection with declared mount points |
|
| `ctx.ui.replace()` / `ctx.ui.inject()` | Hook 6: Surface Injection with declared mount points |
|
||||||
| `ctx.surfaces.register()` | Manifest `surfaces` field → page engine route registration |
|
| `ctx.surfaces.register()` | Manifest `surfaces` field → page engine route registration |
|
||||||
@@ -688,17 +709,22 @@ post-render hooks use to discover display content from any extension.
|
|||||||
|
|
||||||
This does not prescribe version numbers. It describes dependency order.
|
This does not prescribe version numbers. It describes dependency order.
|
||||||
|
|
||||||
**Phase 1: Primitive consolidation**
|
**Phase 1: Primitive consolidation** ✅ (v0.25.1)
|
||||||
- Audit `ui-primitives.js`, `ui-primitives-additions.js`, and Go
|
- `ui-primitives-additions.js` deleted, survivors merged into
|
||||||
template components
|
`ui-primitives.js`
|
||||||
- Define the primitive catalog and factory API
|
- `esc()` centralized (13 private copies eliminated)
|
||||||
- Define the CSS custom property contract (theme API)
|
- Badge system unified (24 scattered classes → grouped in primitives.css)
|
||||||
- Migrate existing callers to the consolidated API
|
- CSS variable names normalized, duplicate selectors resolved
|
||||||
|
- `showConfirm()` is the sole confirm pattern (bare `confirm()` purged)
|
||||||
|
|
||||||
**Phase 2: Component formalization**
|
**Phase 2: Component formalization** ✅ (v0.25.1)
|
||||||
- ChatPane already exists (v0.22.7). Formalize the instance pattern.
|
- `createComponentRegistry()` + `componentMixin()` extracted
|
||||||
- Extract NoteEditor, FileTree, ModelSelector as components
|
- 6 components (ChatPane, ModelSelector, UserMenu, CodeEditor, FileTree,
|
||||||
- Each component gets a Go template partial + JS hydration
|
NoteEditor) use the shared mixin. ~100 lines of boilerplate eliminated.
|
||||||
|
- Each component has a Go template partial + JS hydration
|
||||||
|
- `surface-nav.js` deleted; navigation is native in `ui-core.js`
|
||||||
|
- Vendor scripts (`marked.min.js`, `purify.min.js`) and `ui-format.js`
|
||||||
|
moved to `base.html` (loaded once, available to all surfaces)
|
||||||
|
|
||||||
**Phase 3: Extension hooks**
|
**Phase 3: Extension hooks**
|
||||||
- `Extensions.boot()` on every surface (v0.22.8)
|
- `Extensions.boot()` on every surface (v0.22.8)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Surface Development Guide
|
# Surface Development Guide
|
||||||
|
|
||||||
**Version:** 0.25.0
|
**Version:** 0.25.1
|
||||||
**Audience:** Anyone with admin access to a Chat Switchboard instance
|
**Audience:** Anyone with admin access to a Chat Switchboard instance
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -90,7 +90,9 @@
|
|||||||
</script>
|
</script>
|
||||||
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/events.js?v={{.Version}}"></script>
|
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/events.js?v={{.Version}}"></script>
|
||||||
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/ui-primitives.js?v={{.Version}}"></script>
|
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/ui-primitives.js?v={{.Version}}"></script>
|
||||||
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/ui-primitives-additions.js?v={{.Version}}"></script>
|
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/vendor/marked.min.js"></script>
|
||||||
|
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/vendor/purify.min.js"></script>
|
||||||
|
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/ui-format.js?v={{.Version}}"></script>
|
||||||
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/ui-core.js?v={{.Version}}"></script>
|
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/ui-core.js?v={{.Version}}"></script>
|
||||||
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/pages.js?v={{.Version}}"></script>
|
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/pages.js?v={{.Version}}"></script>
|
||||||
{{/* v0.25.0: Component scripts — available on all surfaces */}}
|
{{/* v0.25.0: Component scripts — available on all surfaces */}}
|
||||||
@@ -156,6 +158,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/debug.js?v={{.Version}}"></script>
|
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/debug.js?v={{.Version}}"></script>
|
||||||
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/repl.js?v={{.Version}}"></script>
|
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/repl.js?v={{.Version}}"></script>
|
||||||
|
<div id="toastContainer" class="toast-container"></div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@@ -77,25 +77,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="toastContainer" class="toast-container"></div>
|
|
||||||
|
|
||||||
{{/* Confirm dialog */}}
|
|
||||||
<div id="confirmModal" class="modal-overlay">
|
|
||||||
<div class="modal" style="max-width:380px;">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h2 id="confirmTitle">Confirm</h2>
|
|
||||||
<button class="modal-close" onclick="closeModal('confirmModal')">✕</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<p id="confirmMessage" style="color:var(--text-2);font-size:14px;line-height:1.65;"></p>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button class="btn-md btn-ghost" onclick="closeModal('confirmModal')">Cancel</button>
|
|
||||||
<button id="confirmOkBtn" class="btn-md btn-danger">Confirm</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{/* Create User modal */}}
|
{{/* Create User modal */}}
|
||||||
<div id="createUserModal" class="modal-overlay">
|
<div id="createUserModal" class="modal-overlay">
|
||||||
<div class="modal" style="max-width:440px;">
|
<div class="modal" style="max-width:440px;">
|
||||||
@@ -229,9 +210,6 @@
|
|||||||
|
|
||||||
{{/* Scripts */}}
|
{{/* Scripts */}}
|
||||||
{{define "scripts-admin"}}
|
{{define "scripts-admin"}}
|
||||||
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/vendor/marked.min.js"></script>
|
|
||||||
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/vendor/purify.min.js"></script>
|
|
||||||
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/ui-format.js?v={{.Version}}"></script>
|
|
||||||
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/ui-settings.js?v={{.Version}}"></script>
|
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/ui-settings.js?v={{.Version}}"></script>
|
||||||
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/ui-admin.js?v={{.Version}}"></script>
|
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/ui-admin.js?v={{.Version}}"></script>
|
||||||
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/admin-handlers.js?v={{.Version}}"></script>
|
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/admin-handlers.js?v={{.Version}}"></script>
|
||||||
|
|||||||
@@ -334,9 +334,6 @@ window.addEventListener('unhandledrejection', function(e) {
|
|||||||
{{/* Side panel overlay (mobile) */}}
|
{{/* Side panel overlay (mobile) */}}
|
||||||
<div id="sidePanelOverlay" class="side-panel-overlay" style="display:none;"></div>
|
<div id="sidePanelOverlay" class="side-panel-overlay" style="display:none;"></div>
|
||||||
|
|
||||||
{{/* ── Toast Container ─────────────────────── */}}
|
|
||||||
<div id="toastContainer" class="toast-container"></div>
|
|
||||||
|
|
||||||
{{/* ── Lightbox ────────────────────────────── */}}
|
{{/* ── Lightbox ────────────────────────────── */}}
|
||||||
<div id="lightbox" class="lightbox" onclick="closeLightbox()">
|
<div id="lightbox" class="lightbox" onclick="closeLightbox()">
|
||||||
<img id="lightboxImg" src="" alt="">
|
<img id="lightboxImg" src="" alt="">
|
||||||
@@ -353,23 +350,7 @@ window.addEventListener('unhandledrejection', function(e) {
|
|||||||
{{/* ── Hidden File Input ─────── */}}
|
{{/* ── Hidden File Input ─────── */}}
|
||||||
<input type="file" id="fileInput" multiple style="display:none;">
|
<input type="file" id="fileInput" multiple style="display:none;">
|
||||||
|
|
||||||
{{/* ── Settings Modal ──────────────────────── */}}
|
{{/* ── Team Admin Modal ────────────────────── */}}
|
||||||
<div id="settingsModal" class="modal-overlay">
|
|
||||||
<div class="modal-content modal-lg">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h2>Settings</h2>
|
|
||||||
<button id="settingsCloseBtn" class="modal-close" onclick="closeModal('settingsModal')">✕</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-tabs">
|
|
||||||
<button class="settings-tab active" data-stab="general" onclick="UI.switchSettingsTab('general')">General</button>
|
|
||||||
<button class="settings-tab" data-stab="appearance" onclick="UI.switchSettingsTab('appearance')">Appearance</button>
|
|
||||||
<button id="settingsProvidersTabBtn" class="settings-tab" data-stab="providers" onclick="UI.switchSettingsTab('providers')">My Providers</button>
|
|
||||||
<button id="settingsPersonasTabBtn" class="settings-tab" data-stab="personas" onclick="UI.switchSettingsTab('personas')">My Personas</button>
|
|
||||||
<button id="settingsUsageTabBtn" class="settings-tab" data-stab="usage" onclick="UI.switchSettingsTab('usage')">Usage</button>
|
|
||||||
<button class="settings-tab" data-stab="profile" onclick="UI.switchSettingsTab('profile')">Profile</button>
|
|
||||||
<button id="settingsRolesTabBtn" class="settings-tab" data-stab="roles" onclick="UI.switchSettingsTab('roles')" style="display:none;">Roles</button>
|
|
||||||
<button id="settingsKBTabBtn" class="settings-tab" data-stab="kb" onclick="UI.switchSettingsTab('kb')" style="display:none;">Knowledge</button>
|
|
||||||
<button id="settingsTeamsSection" class="settings-tab" data-stab="teams" onclick="UI.switchSettingsTab('teams')">Teams</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
{{/* General tab */}}
|
{{/* General tab */}}
|
||||||
@@ -730,7 +711,6 @@ window.addEventListener('unhandledrejection', function(e) {
|
|||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{define "css-chat"}}
|
{{define "css-chat"}}
|
||||||
<link rel="stylesheet" href="{{.BasePath}}/css/editor-mode.css?v={{.Version}}">
|
|
||||||
<link rel="stylesheet" href="{{.BasePath}}/css/persona-kb.css?v={{.Version}}">
|
<link rel="stylesheet" href="{{.BasePath}}/css/persona-kb.css?v={{.Version}}">
|
||||||
<link rel="stylesheet" href="{{.BasePath}}/css/memory.css?v={{.Version}}">
|
<link rel="stylesheet" href="{{.BasePath}}/css/memory.css?v={{.Version}}">
|
||||||
<link rel="stylesheet" href="{{.BasePath}}/css/notifications.css?v={{.Version}}">
|
<link rel="stylesheet" href="{{.BasePath}}/css/notifications.css?v={{.Version}}">
|
||||||
@@ -740,15 +720,11 @@ window.addEventListener('unhandledrejection', function(e) {
|
|||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{define "scripts-chat"}}
|
{{define "scripts-chat"}}
|
||||||
{{/* Vendor libs */}}
|
{{/* CM6 bundle — chat-specific (code input, extension editor) */}}
|
||||||
<script nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/vendor/marked.min.js" onerror="this.onerror=null;this.src='https://cdnjs.cloudflare.com/ajax/libs/marked/16.3.0/lib/marked.umd.min.js'"></script>
|
|
||||||
<script nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/vendor/purify.min.js" onerror="this.onerror=null;this.src='https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.2.4/purify.min.js'"></script>
|
|
||||||
<script nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/vendor/codemirror/codemirror.bundle.js?v={{$.Version}}" onerror="console.warn('[CM6] Bundle not available — falling back to textarea')"></script>
|
<script nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/vendor/codemirror/codemirror.bundle.js?v={{$.Version}}" onerror="console.warn('[CM6] Bundle not available — falling back to textarea')"></script>
|
||||||
{{/* App JS — order matches index.html (minus api/events/ui-primitives already in base) */}}
|
{{/* App JS — order matches index.html (minus api/events/ui-primitives/ui-format already in base) */}}
|
||||||
{{/* debug.js and repl.js now loaded in base.html for all surfaces */}}
|
|
||||||
<script nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/extensions.js?v={{$.Version}}"></script>
|
<script nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/extensions.js?v={{$.Version}}"></script>
|
||||||
<script nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/panels.js?v={{$.Version}}"></script>
|
<script nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/panels.js?v={{$.Version}}"></script>
|
||||||
<script nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/ui-format.js?v={{$.Version}}"></script>
|
|
||||||
<script nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/ui-settings.js?v={{$.Version}}"></script>
|
<script nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/ui-settings.js?v={{$.Version}}"></script>
|
||||||
<script nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/ui-admin.js?v={{$.Version}}"></script>
|
<script nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/ui-admin.js?v={{$.Version}}"></script>
|
||||||
<script nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/tokens.js?v={{$.Version}}"></script>
|
<script nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/tokens.js?v={{$.Version}}"></script>
|
||||||
@@ -766,7 +742,5 @@ window.addEventListener('unhandledrejection', function(e) {
|
|||||||
<script nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/chat.js?v={{$.Version}}"></script>
|
<script nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/chat.js?v={{$.Version}}"></script>
|
||||||
<script nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/settings-handlers.js?v={{$.Version}}"></script>
|
<script nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/settings-handlers.js?v={{$.Version}}"></script>
|
||||||
<script nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/admin-handlers.js?v={{$.Version}}"></script>
|
<script nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/admin-handlers.js?v={{$.Version}}"></script>
|
||||||
<script nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/surface-nav.js?v={{$.Version}}"></script>
|
|
||||||
<script nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/editor-mode.js?v={{$.Version}}"></script>
|
|
||||||
<script nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/app.js?v={{$.Version}}"></script>
|
<script nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/app.js?v={{$.Version}}"></script>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@@ -83,8 +83,6 @@
|
|||||||
{{template "note-editor" dict "ID" "edNotes"}}
|
{{template "note-editor" dict "ID" "edNotes"}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="toastContainer" class="toast-container"></div>
|
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{/* ── CSS ─────────────────────────────────── */}}
|
{{/* ── CSS ─────────────────────────────────── */}}
|
||||||
@@ -94,10 +92,7 @@
|
|||||||
|
|
||||||
{{/* ── Scripts ─────────────────────────────── */}}
|
{{/* ── Scripts ─────────────────────────────── */}}
|
||||||
{{define "scripts-editor"}}
|
{{define "scripts-editor"}}
|
||||||
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/vendor/marked.min.js"></script>
|
|
||||||
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/vendor/purify.min.js"></script>
|
|
||||||
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/vendor/codemirror/codemirror.bundle.js?v={{.Version}}" onerror="console.warn('[CM6] Bundle not available')"></script>
|
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/vendor/codemirror/codemirror.bundle.js?v={{.Version}}" onerror="console.warn('[CM6] Bundle not available')"></script>
|
||||||
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/ui-format.js?v={{.Version}}"></script>
|
|
||||||
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/chat-pane.js?v={{.Version}}"></script>
|
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/chat-pane.js?v={{.Version}}"></script>
|
||||||
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/editor-surface.js?v={{.Version}}"></script>
|
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/editor-surface.js?v={{.Version}}"></script>
|
||||||
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/app.js?v={{.Version}}"></script>
|
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/app.js?v={{.Version}}"></script>
|
||||||
|
|||||||
@@ -16,49 +16,14 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{define "css-notes"}}
|
{{define "css-notes"}}
|
||||||
<link rel="stylesheet" href="{{.BasePath}}/css/editor-mode.css?v={{.Version}}">
|
|
||||||
<link rel="stylesheet" href="{{.BasePath}}/css/persona-kb.css?v={{.Version}}">
|
<link rel="stylesheet" href="{{.BasePath}}/css/persona-kb.css?v={{.Version}}">
|
||||||
<link rel="stylesheet" href="{{.BasePath}}/css/memory.css?v={{.Version}}">
|
<link rel="stylesheet" href="{{.BasePath}}/css/memory.css?v={{.Version}}">
|
||||||
<link rel="stylesheet" href="{{.BasePath}}/css/notifications.css?v={{.Version}}">
|
<link rel="stylesheet" href="{{.BasePath}}/css/notifications.css?v={{.Version}}">
|
||||||
<link rel="stylesheet" href="{{.BasePath}}/css/channel-models.css?v={{.Version}}">
|
<link rel="stylesheet" href="{{.BasePath}}/css/channel-models.css?v={{.Version}}">
|
||||||
<link rel="stylesheet" href="{{.BasePath}}/branding/custom.css" onerror="this.remove()">
|
<link rel="stylesheet" href="{{.BasePath}}/branding/custom.css" onerror="this.remove()">
|
||||||
<style>
|
|
||||||
.surface-notes {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.surface-notes-body {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
min-height: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.surface-notes-main {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
min-width: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.surface-notes-assist {
|
|
||||||
width: 400px;
|
|
||||||
border-left: 1px solid var(--border, #2a2a2a);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.surface-notes-assist { display: none; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{define "scripts-notes"}}
|
{{define "scripts-notes"}}
|
||||||
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/vendor/marked.min.js"></script>
|
|
||||||
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/vendor/purify.min.js"></script>
|
|
||||||
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/ui-format.js?v={{.Version}}"></script>
|
|
||||||
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/chat.js?v={{.Version}}"></script>
|
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/chat.js?v={{.Version}}"></script>
|
||||||
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/files.js?v={{.Version}}"></script>
|
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/files.js?v={{.Version}}"></script>
|
||||||
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/channel-models.js?v={{.Version}}"></script>
|
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/channel-models.js?v={{.Version}}"></script>
|
||||||
|
|||||||
@@ -89,9 +89,9 @@
|
|||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
<h3>Theme</h3>
|
<h3>Theme</h3>
|
||||||
<div id="themeToggle" class="toggle-group">
|
<div id="themeToggle" class="toggle-group">
|
||||||
<button class="theme-btn" data-theme="light">☀ Light</button>
|
<button class="toggle-btn" data-theme="light">☀ Light</button>
|
||||||
<button class="theme-btn" data-theme="dark">☾ Dark</button>
|
<button class="toggle-btn" data-theme="dark">☾ Dark</button>
|
||||||
<button class="theme-btn" data-theme="system">⊞ System</button>
|
<button class="toggle-btn" data-theme="system">⊞ System</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
@@ -150,8 +150,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="toastContainer" class="toast-container"></div>
|
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{/* CSS: loaded via base.html (variables, layout, primitives, modals, chat, panels, surfaces, splash) */}}
|
{{/* CSS: loaded via base.html (variables, layout, primitives, modals, chat, panels, surfaces, splash) */}}
|
||||||
@@ -159,9 +157,6 @@
|
|||||||
|
|
||||||
{{/* Scripts */}}
|
{{/* Scripts */}}
|
||||||
{{define "scripts-settings"}}
|
{{define "scripts-settings"}}
|
||||||
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/vendor/marked.min.js"></script>
|
|
||||||
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/vendor/purify.min.js"></script>
|
|
||||||
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/ui-format.js?v={{.Version}}"></script>
|
|
||||||
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/ui-settings.js?v={{.Version}}"></script>
|
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/ui-settings.js?v={{.Version}}"></script>
|
||||||
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/settings-handlers.js?v={{.Version}}"></script>
|
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/settings-handlers.js?v={{.Version}}"></script>
|
||||||
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/knowledge-ui.js?v={{.Version}}"></script>
|
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/knowledge-ui.js?v={{.Version}}"></script>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.admin-surface-row:hover {
|
.admin-surface-row:hover {
|
||||||
background: var(--bg-tertiary, #1e1e22);
|
background: var(--bg-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-surface-info {
|
.admin-surface-info {
|
||||||
|
|||||||
@@ -53,7 +53,7 @@
|
|||||||
}
|
}
|
||||||
.ch-model-pill-remove:hover {
|
.ch-model-pill-remove:hover {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
color: var(--error-color, #d32f2f);
|
color: var(--danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ch-model-add-btn {
|
.ch-model-add-btn {
|
||||||
@@ -151,7 +151,7 @@
|
|||||||
.mention-ac-handle {
|
.mention-ac-handle {
|
||||||
font-size: 0.72rem;
|
font-size: 0.72rem;
|
||||||
color: var(--accent, #6c9fff);
|
color: var(--accent, #6c9fff);
|
||||||
font-family: var(--font-mono, monospace);
|
font-family: var(--mono);
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
.mention-ac-model {
|
.mention-ac-model {
|
||||||
|
|||||||
@@ -164,13 +164,6 @@
|
|||||||
color: var(--text-3);
|
color: var(--text-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-btn {
|
|
||||||
background: none; border: none; color: var(--text-3); cursor: pointer;
|
|
||||||
padding: 4px; border-radius: 4px;
|
|
||||||
transition: color var(--transition);
|
|
||||||
}
|
|
||||||
.icon-btn:hover { color: var(--text); }
|
|
||||||
|
|
||||||
/* Messages — Bubble Layout (matching prototype) */
|
/* Messages — Bubble Layout (matching prototype) */
|
||||||
.messages { flex: 1; overflow-y: auto; scroll-behavior: smooth; }
|
.messages { flex: 1; overflow-y: auto; scroll-behavior: smooth; }
|
||||||
|
|
||||||
@@ -650,9 +643,9 @@
|
|||||||
|
|
||||||
/* ── Chat type indicators (v0.23.0) ────────── */
|
/* ── Chat type indicators (v0.23.0) ────────── */
|
||||||
.chat-type-icon { font-size: 11px; opacity: 0.7; }
|
.chat-type-icon { font-size: 11px; opacity: 0.7; }
|
||||||
.mono-input { font-family: var(--font-mono, 'JetBrains Mono', monospace) !important; font-size: 12px !important; }
|
.mono-input { font-family: var(--mono) !important; font-size: 12px !important; }
|
||||||
|
|
||||||
/* Handle hints in dropdowns and pickers */
|
/* Handle hints in dropdowns and pickers */
|
||||||
.item-handle { font-size: 10px; color: var(--accent); font-family: var(--font-mono, monospace); margin-left: 6px; opacity: 0.75; }
|
.item-handle { font-size: 10px; color: var(--accent); font-family: var(--mono); margin-left: 6px; opacity: 0.75; }
|
||||||
.group-persona-handle { font-size: 10px; color: var(--accent); font-family: var(--font-mono, monospace); margin-left: 4px; opacity: 0.75; }
|
.group-persona-handle { font-size: 10px; color: var(--accent); font-family: var(--mono); margin-left: 4px; opacity: 0.75; }
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@
|
|||||||
|
|
||||||
.editor-topbar-back:hover {
|
.editor-topbar-back:hover {
|
||||||
color: var(--text, #eee);
|
color: var(--text, #eee);
|
||||||
background: var(--bg-tertiary, #1e1e22);
|
background: var(--bg-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-topbar-sep {
|
.editor-topbar-sep {
|
||||||
@@ -86,7 +86,7 @@
|
|||||||
|
|
||||||
.editor-ws-selector-btn:hover {
|
.editor-ws-selector-btn:hover {
|
||||||
border-color: var(--border, #2a2a2e);
|
border-color: var(--border, #2a2a2e);
|
||||||
background: var(--bg-tertiary, #1e1e22);
|
background: var(--bg-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-ws-dropdown {
|
.editor-ws-dropdown {
|
||||||
@@ -128,7 +128,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.editor-ws-dropdown-item:hover {
|
.editor-ws-dropdown-item:hover {
|
||||||
background: var(--bg-tertiary, #1e1e22);
|
background: var(--bg-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-ws-dropdown-item.active {
|
.editor-ws-dropdown-item.active {
|
||||||
@@ -247,7 +247,7 @@
|
|||||||
|
|
||||||
.editor-bootstrap-ws-item:hover {
|
.editor-bootstrap-ws-item:hover {
|
||||||
border-color: var(--accent, #b38a4e);
|
border-color: var(--accent, #b38a4e);
|
||||||
background: var(--bg-tertiary, #1e1e22);
|
background: var(--bg-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-bootstrap-ws-name {
|
.editor-bootstrap-ws-name {
|
||||||
@@ -307,7 +307,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.surface-editor .file-tree-row:hover {
|
.surface-editor .file-tree-row:hover {
|
||||||
background: var(--bg-tertiary, #1e1e22);
|
background: var(--bg-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.surface-editor .file-tree-row.active {
|
.surface-editor .file-tree-row.active {
|
||||||
@@ -361,7 +361,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.file-tree-ctx-item:hover {
|
.file-tree-ctx-item:hover {
|
||||||
background: var(--bg-tertiary, #1e1e22);
|
background: var(--bg-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── CodeEditor overrides ──────────────────── */
|
/* ── CodeEditor overrides ──────────────────── */
|
||||||
@@ -399,7 +399,7 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.surface-editor .code-editor-tab:hover { background: var(--bg-tertiary, #1e1e22); }
|
.surface-editor .code-editor-tab:hover { background: var(--bg-hover); }
|
||||||
.surface-editor .code-editor-tab.active { color: var(--text, #eee); background: var(--bg, #0e0e10); }
|
.surface-editor .code-editor-tab.active { color: var(--text, #eee); background: var(--bg, #0e0e10); }
|
||||||
.surface-editor .code-editor-tab.modified .code-editor-tab-modified { color: var(--warning, #e5a842); }
|
.surface-editor .code-editor-tab.modified .code-editor-tab-modified { color: var(--warning, #e5a842); }
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
background: var(--bg-2);
|
background: var(--bg-raised);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
min-width: 80px;
|
min-width: 80px;
|
||||||
}
|
}
|
||||||
@@ -93,7 +93,7 @@
|
|||||||
/* ── Memory Card ───────────────────────── */
|
/* ── Memory Card ───────────────────────── */
|
||||||
|
|
||||||
.memory-card {
|
.memory-card {
|
||||||
background: var(--bg-2);
|
background: var(--bg-raised);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
|
|||||||
@@ -110,7 +110,6 @@
|
|||||||
.provider-name { font-weight: 500; font-size: 13px; }
|
.provider-name { font-weight: 500; font-size: 13px; }
|
||||||
.provider-meta { font-size: 11px; color: var(--text-3); margin-left: 8px; }
|
.provider-meta { font-size: 11px; color: var(--text-3); margin-left: 8px; }
|
||||||
.provider-actions { display: flex; align-items: center; gap: 8px; }
|
.provider-actions { display: flex; align-items: center; gap: 8px; }
|
||||||
.badge-global { font-size: 10px; color: var(--text-3); background: var(--bg-raised); padding: 2px 8px; border-radius: 4px; }
|
|
||||||
|
|
||||||
/* Admin panel (fullscreen) */
|
/* Admin panel (fullscreen) */
|
||||||
.admin-panel {
|
.admin-panel {
|
||||||
@@ -232,7 +231,6 @@
|
|||||||
.admin-persona-row .persona-actions { display: flex; gap: 6px; align-items: center; flex-shrink: 0; }
|
.admin-persona-row .persona-actions { display: flex; gap: 6px; align-items: center; flex-shrink: 0; }
|
||||||
.admin-persona-row .btn-delete { background: none; border: none; color: var(--text-3); cursor: pointer; font-size: 16px; padding: 2px 6px; }
|
.admin-persona-row .btn-delete { background: none; border: none; color: var(--text-3); cursor: pointer; font-size: 16px; padding: 2px 6px; }
|
||||||
.admin-persona-row .btn-delete:hover { color: var(--danger); }
|
.admin-persona-row .btn-delete:hover { color: var(--danger); }
|
||||||
.badge-user { font-size: 10px; padding: 1px 6px; border-radius: 3px; background: var(--accent-dim); color: var(--accent); }
|
|
||||||
|
|
||||||
/* Stats cards */
|
/* Stats cards */
|
||||||
.stats-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 12px; }
|
.stats-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 12px; }
|
||||||
@@ -247,12 +245,12 @@
|
|||||||
.audit-row { padding: 6px 0; border-bottom: 1px solid var(--border); font-size: 13px; }
|
.audit-row { padding: 6px 0; border-bottom: 1px solid var(--border); font-size: 13px; }
|
||||||
.audit-row:last-child { border-bottom: none; }
|
.audit-row:last-child { border-bottom: none; }
|
||||||
.audit-main { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
.audit-main { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
||||||
.audit-action { font-weight: 600; color: var(--accent); font-family: var(--font-mono, monospace); font-size: 12px; }
|
.audit-action { font-weight: 600; color: var(--accent); font-family: var(--mono); font-size: 12px; }
|
||||||
.audit-actor { color: var(--text); }
|
.audit-actor { color: var(--text); }
|
||||||
.audit-resource { color: var(--text-3); font-family: var(--font-mono, monospace); font-size: 11px; }
|
.audit-resource { color: var(--text-3); font-family: var(--mono); font-size: 11px; }
|
||||||
.audit-meta { display: flex; gap: 10px; margin-top: 2px; font-size: 11px; color: var(--text-3); flex-wrap: wrap; }
|
.audit-meta { display: flex; gap: 10px; margin-top: 2px; font-size: 11px; color: var(--text-3); flex-wrap: wrap; }
|
||||||
.audit-details { color: var(--text-2); }
|
.audit-details { color: var(--text-2); }
|
||||||
.audit-ip { font-family: var(--font-mono, monospace); }
|
.audit-ip { font-family: var(--mono); }
|
||||||
.pagination-row { display: flex; align-items: center; justify-content: center; gap: 12px; }
|
.pagination-row { display: flex; align-items: center; justify-content: center; gap: 12px; }
|
||||||
|
|
||||||
/* Banner preview */
|
/* Banner preview */
|
||||||
@@ -285,11 +283,6 @@
|
|||||||
.model-list-item .model-provider { font-size: 12px; color: var(--text-3); }
|
.model-list-item .model-provider { font-size: 12px; color: var(--text-3); }
|
||||||
.model-list-item .model-caps-inline { display: flex; gap: 3px; }
|
.model-list-item .model-caps-inline { display: flex; gap: 3px; }
|
||||||
|
|
||||||
.badge-admin { background: var(--accent); color: var(--text-on-color); font-size: 10px; padding: 1px 8px; border-radius: 4px; }
|
|
||||||
.badge-user { background: var(--bg-raised); color: var(--text-3); font-size: 10px; padding: 1px 8px; border-radius: 4px; }
|
|
||||||
.badge-inactive { background: var(--danger); color: var(--text-on-color); font-size: 10px; padding: 1px 8px; border-radius: 4px; }
|
|
||||||
.badge-pending { background: var(--warning); color: var(--text-on-warning); font-size: 10px; padding: 1px 8px; border-radius: 4px; }
|
|
||||||
.badge-team { background: var(--accent-dim); color: var(--accent-light); font-size: 10px; padding: 1px 8px; border-radius: 4px; margin-left: 2px; }
|
|
||||||
.admin-approve-form { padding: 10px 12px; margin: -4px 0 8px; background: var(--glass); border-radius: 0 0 8px 8px; border-top: 1px solid var(--border); }
|
.admin-approve-form { padding: 10px 12px; margin: -4px 0 8px; background: var(--glass); border-radius: 0 0 8px 8px; border-top: 1px solid var(--border); }
|
||||||
.admin-approve-form .checkbox-label { display: block; margin: 2px 0; }
|
.admin-approve-form .checkbox-label { display: block; margin: 2px 0; }
|
||||||
.btn-approve { background: var(--success-dim); color: var(--success-light); border: 1px solid rgba(34,197,94,0.3); border-radius: 4px; padding: 3px 10px; cursor: pointer; font-size: 12px; }
|
.btn-approve { background: var(--success-dim); color: var(--success-light); border: 1px solid rgba(34,197,94,0.3); border-radius: 4px; padding: 3px 10px; cursor: pointer; font-size: 12px; }
|
||||||
@@ -301,22 +294,12 @@
|
|||||||
}
|
}
|
||||||
.team-admin-back:hover { opacity: 1; }
|
.team-admin-back:hover { opacity: 1; }
|
||||||
.team-tab-content { padding: 0; }
|
.team-tab-content { padding: 0; }
|
||||||
.badge-private { background: var(--purple); color: var(--text-on-color); font-size: 10px; padding: 1px 8px; border-radius: 4px; }
|
|
||||||
.badge-success { background: var(--success-dim); color: var(--success-light); font-size: 10px; padding: 1px 8px; border-radius: 4px; }
|
|
||||||
.badge-warning { background: var(--warning-dim); color: var(--warning-light); font-size: 10px; padding: 1px 8px; border-radius: 4px; }
|
|
||||||
.badge-danger { background: var(--danger-dim); color: var(--danger-light); font-size: 10px; padding: 1px 8px; border-radius: 4px; }
|
|
||||||
|
|
||||||
/* Health + Routing admin (v0.22.3) */
|
/* Health + Routing admin (v0.22.3) */
|
||||||
.admin-health-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 12px; }
|
.admin-health-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 12px; }
|
||||||
.admin-health-card { background: var(--bg-2); border: 1px solid var(--border); border-radius: 8px; padding: 12px; }
|
.admin-health-card { background: var(--bg-raised); border: 1px solid var(--border); border-radius: 8px; padding: 12px; }
|
||||||
.list-card { background: var(--bg-2); border: 1px solid var(--border); border-radius: 8px; padding: 12px; margin-bottom: 8px; }
|
.list-card { background: var(--bg-raised); border: 1px solid var(--border); border-radius: 8px; padding: 12px; margin-bottom: 8px; }
|
||||||
.admin-table { border-collapse: collapse; }
|
|
||||||
.admin-table th, .admin-table td { padding: 6px 10px; text-align: left; border-bottom: 1px solid var(--border); }
|
|
||||||
.admin-table th { font-weight: 600; color: var(--text-2); font-size: 11px; text-transform: uppercase; }
|
|
||||||
.admin-user-row.user-inactive { opacity: 0.7; }
|
.admin-user-row.user-inactive { opacity: 0.7; }
|
||||||
.loading { color: var(--text-3); font-size: 13px; padding: 0.5rem; }
|
|
||||||
.error-hint { color: var(--danger); font-size: 13px; padding: 0.5rem; }
|
|
||||||
.empty-hint { color: var(--text-3); font-size: 13px; text-align: center; padding: 1rem; }
|
|
||||||
|
|
||||||
|
|
||||||
/* ── Debug Modal ─────────────────────────── */
|
/* ── Debug Modal ─────────────────────────── */
|
||||||
@@ -455,7 +438,7 @@
|
|||||||
.group-persona-item:hover { background: var(--bg-hover); }
|
.group-persona-item:hover { background: var(--bg-hover); }
|
||||||
.group-persona-item input[type="checkbox"] { accent-color: var(--accent); flex-shrink: 0; }
|
.group-persona-item input[type="checkbox"] { accent-color: var(--accent); flex-shrink: 0; }
|
||||||
.group-persona-name { flex: 1; color: var(--text); font-weight: 500; }
|
.group-persona-name { flex: 1; color: var(--text); font-weight: 500; }
|
||||||
.group-persona-model { font-size: 11px; color: var(--text-3); font-family: var(--font-mono); }
|
.group-persona-model { font-size: 11px; color: var(--text-3); font-family: var(--mono); }
|
||||||
.chain-typing-name { font-size: 11px; font-weight: 600; color: var(--accent); margin-right: 6px; }
|
.chain-typing-name { font-size: 11px; font-weight: 600; color: var(--accent); margin-right: 6px; }
|
||||||
|
|
||||||
/* ── Creation Dialog (project / channel / folder — v0.23.1) ── */
|
/* ── Creation Dialog (project / channel / folder — v0.23.1) ── */
|
||||||
@@ -486,11 +469,3 @@
|
|||||||
.sb-dialog-color-label { font-size: 12px; color: var(--text-2); }
|
.sb-dialog-color-label { font-size: 12px; color: var(--text-2); }
|
||||||
.sb-dialog-color-input { width: 32px; height: 26px; border: 1px solid var(--border); border-radius: var(--radius); cursor: pointer; padding: 1px; background: none; }
|
.sb-dialog-color-input { width: 32px; height: 26px; border: 1px solid var(--border); border-radius: var(--radius); cursor: pointer; padding: 1px; background: none; }
|
||||||
.sb-dialog-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
.sb-dialog-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
||||||
.sb-dialog-btn {
|
|
||||||
padding: 6px 14px; border-radius: var(--radius); font-size: 12px; font-family: inherit;
|
|
||||||
cursor: pointer; border: 1px solid var(--border);
|
|
||||||
background: var(--bg-hover); color: var(--text-2);
|
|
||||||
}
|
|
||||||
.sb-dialog-btn:hover { background: var(--bg-raised); }
|
|
||||||
.sb-dialog-btn-primary { background: var(--accent); color: #fff; border-color: var(--accent); }
|
|
||||||
.sb-dialog-btn-primary:hover { opacity: 0.88; }
|
|
||||||
|
|||||||
@@ -10,9 +10,9 @@
|
|||||||
.notif-pref-table th {
|
.notif-pref-table th {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
border-bottom: 2px solid var(--border-color, #ddd);
|
border-bottom: 2px solid var(--border);
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
color: var(--text-secondary, #888);
|
color: var(--text-2);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
.notif-pref-table th:nth-child(2),
|
.notif-pref-table th:nth-child(2),
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
}
|
}
|
||||||
.notif-pref-table td {
|
.notif-pref-table td {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
border-bottom: 1px solid var(--border-color, #eee);
|
border-bottom: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
.notif-pref-toggle {
|
.notif-pref-toggle {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -52,10 +52,10 @@
|
|||||||
}
|
}
|
||||||
.smtp-test-status {
|
.smtp-test-status {
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
color: var(--text-secondary, #888);
|
color: var(--text-2);
|
||||||
}
|
}
|
||||||
.smtp-test-status.success { color: var(--success-color, #2e7d32); }
|
.smtp-test-status.success { color: var(--success); }
|
||||||
.smtp-test-status.error { color: var(--error-color, #d32f2f); }
|
.smtp-test-status.error { color: var(--danger); }
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
.smtp-config-grid {
|
.smtp-config-grid {
|
||||||
|
|||||||
@@ -314,10 +314,6 @@
|
|||||||
.notes-select-all input { cursor: pointer; }
|
.notes-select-all input { cursor: pointer; }
|
||||||
|
|
||||||
.notes-list { padding: 8px; }
|
.notes-list { padding: 8px; }
|
||||||
.notes-empty, .notes-loading {
|
|
||||||
text-align: center; color: var(--text-3); padding: 2rem;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-item {
|
.note-item {
|
||||||
display: flex; align-items: flex-start; gap: 8px;
|
display: flex; align-items: flex-start; gap: 8px;
|
||||||
@@ -475,7 +471,7 @@
|
|||||||
background: var(--bg-surface);
|
background: var(--bg-surface);
|
||||||
}
|
}
|
||||||
.transclusion-header {
|
.transclusion-header {
|
||||||
padding: 6px 10px; background: var(--bg-2);
|
padding: 6px 10px; background: var(--bg-raised);
|
||||||
border-bottom: 1px solid var(--border); font-size: 12px;
|
border-bottom: 1px solid var(--border); font-size: 12px;
|
||||||
}
|
}
|
||||||
.transclusion-content {
|
.transclusion-content {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
.persona-kb-list {
|
.persona-kb-list {
|
||||||
max-height: 200px;
|
max-height: 200px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
border: 1px solid var(--border-color, #333);
|
border: 1px solid var(--border);
|
||||||
border-radius: var(--radius, 6px);
|
border-radius: var(--radius, 6px);
|
||||||
padding: 0.25rem;
|
padding: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,14 +41,6 @@ input[type="password"]:focus, textarea:focus {
|
|||||||
}
|
}
|
||||||
select option { background: var(--bg-surface); color: var(--text); }
|
select option { background: var(--bg-surface); color: var(--text); }
|
||||||
|
|
||||||
.form-group { margin-bottom: 0.75rem; }
|
|
||||||
.form-group label { display: block; font-size: 13px; color: var(--text-2); margin-bottom: 4px; font-weight: 500; }
|
|
||||||
.form-group input, .form-group select, .form-group textarea {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.form-group input:focus, .form-group select:focus, .form-group textarea:focus {
|
|
||||||
outline: none; border-color: var(--accent);
|
|
||||||
}
|
|
||||||
.form-row { display: flex; gap: 0.75rem; }
|
.form-row { display: flex; gap: 0.75rem; }
|
||||||
.form-row .form-group { flex: 1; }
|
.form-row .form-group { flex: 1; }
|
||||||
.form-hint { font-weight: 400; color: var(--text-3); margin-left: 4px; }
|
.form-hint { font-weight: 400; color: var(--text-3); margin-left: 4px; }
|
||||||
@@ -68,25 +60,6 @@ select option { background: var(--bg-surface); color: var(--text); }
|
|||||||
.range-labels { display: flex; justify-content: space-between; font-size: 11px; color: var(--text-3); }
|
.range-labels { display: flex; justify-content: space-between; font-size: 11px; color: var(--text-3); }
|
||||||
.checkbox-label { display: flex; align-items: center; gap: 8px; font-size: 14px; cursor: pointer; margin: 0.5rem 0; color: var(--text-2); }
|
.checkbox-label { display: flex; align-items: center; gap: 8px; font-size: 14px; cursor: pointer; margin: 0.5rem 0; color: var(--text-2); }
|
||||||
|
|
||||||
/* ── Theme Toggle ────────────────────────── */
|
|
||||||
.theme-toggle {
|
|
||||||
display: flex; gap: 4px; padding: 3px;
|
|
||||||
background: var(--bg); border: 1px solid var(--border);
|
|
||||||
border-radius: var(--radius); width: fit-content;
|
|
||||||
}
|
|
||||||
.theme-btn {
|
|
||||||
display: flex; align-items: center; gap: 6px;
|
|
||||||
padding: 6px 14px; border-radius: calc(var(--radius) - 2px);
|
|
||||||
border: none; background: none; color: var(--text-3);
|
|
||||||
font-family: var(--font); font-size: 13px; font-weight: 500;
|
|
||||||
cursor: pointer; transition: all var(--transition);
|
|
||||||
}
|
|
||||||
.theme-btn:hover { color: var(--text); background: var(--bg-hover); }
|
|
||||||
.theme-btn.active {
|
|
||||||
color: var(--text); background: var(--bg-raised);
|
|
||||||
box-shadow: 0 1px 3px rgba(0,0,0,0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* ── Toast ────────────────────────────────── */
|
/* ── Toast ────────────────────────────────── */
|
||||||
|
|
||||||
@@ -320,9 +293,6 @@ select option { background: var(--bg-surface); color: var(--text); }
|
|||||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
.kb-popup-meta { font-size: 11px; color: var(--text-3); }
|
.kb-popup-meta { font-size: 11px; color: var(--text-3); }
|
||||||
.kb-popup-empty, .kb-popup-loading {
|
|
||||||
padding: 16px; text-align: center; font-size: 13px; color: var(--text-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* KB management panel (settings tab) */
|
/* KB management panel (settings tab) */
|
||||||
.kb-manage-header {
|
.kb-manage-header {
|
||||||
@@ -335,7 +305,7 @@ select option { background: var(--bg-surface); color: var(--text); }
|
|||||||
.kb-manage-item {
|
.kb-manage-item {
|
||||||
display: flex; align-items: center; gap: 8px;
|
display: flex; align-items: center; gap: 8px;
|
||||||
padding: 8px 10px; border-radius: var(--radius);
|
padding: 8px 10px; border-radius: var(--radius);
|
||||||
background: var(--bg-2); transition: background var(--transition);
|
background: var(--bg-raised); transition: background var(--transition);
|
||||||
}
|
}
|
||||||
.kb-manage-item:hover { background: var(--bg-hover); }
|
.kb-manage-item:hover { background: var(--bg-hover); }
|
||||||
.kb-manage-info { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
|
.kb-manage-info { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
|
||||||
@@ -344,9 +314,6 @@ select option { background: var(--bg-surface); color: var(--text); }
|
|||||||
.kb-manage-stats { font-size: 11px; color: var(--text-3); }
|
.kb-manage-stats { font-size: 11px; color: var(--text-3); }
|
||||||
.kb-manage-desc-block { font-size: 12px; color: var(--text-3); margin-bottom: 12px; }
|
.kb-manage-desc-block { font-size: 12px; color: var(--text-3); margin-bottom: 12px; }
|
||||||
.kb-manage-actions { display: flex; gap: 4px; flex-shrink: 0; }
|
.kb-manage-actions { display: flex; gap: 4px; flex-shrink: 0; }
|
||||||
.kb-manage-empty, .kb-manage-loading {
|
|
||||||
padding: 20px; text-align: center; font-size: 13px; color: var(--text-3);
|
|
||||||
}
|
|
||||||
.kb-doc-item[data-doc-status="pending"] .kb-manage-name,
|
.kb-doc-item[data-doc-status="pending"] .kb-manage-name,
|
||||||
.kb-doc-item[data-doc-status="extracting"] .kb-manage-name,
|
.kb-doc-item[data-doc-status="extracting"] .kb-manage-name,
|
||||||
.kb-doc-item[data-doc-status="chunking"] .kb-manage-name,
|
.kb-doc-item[data-doc-status="chunking"] .kb-manage-name,
|
||||||
@@ -363,20 +330,15 @@ select option { background: var(--bg-surface); color: var(--text); }
|
|||||||
color: var(--text-2); margin-bottom: 5px;
|
color: var(--text-2); margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
.form-group input, .form-group select, .form-group textarea {
|
.form-group input, .form-group select, .form-group textarea {
|
||||||
padding: 8px 12px; border-radius: 8px;
|
width: 100%; outline: none;
|
||||||
border: 1px solid var(--border);
|
|
||||||
background: var(--input-bg, var(--bg)); color: var(--text);
|
|
||||||
font-size: 13px; font-family: inherit;
|
|
||||||
width: 100%; max-width: 360px; outline: none;
|
|
||||||
transition: border-color 0.15s;
|
|
||||||
}
|
}
|
||||||
.form-group input:focus, .form-group select:focus, .form-group textarea:focus {
|
.form-group input:focus, .form-group select:focus, .form-group textarea:focus {
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
}
|
}
|
||||||
.form-group input:disabled { opacity: 0.6; }
|
.form-group input:disabled { opacity: 0.6; }
|
||||||
.form-group input[type="range"] { max-width: 100%; border: none; background: none; }
|
.form-group input[type="range"] { border: none; background: none; }
|
||||||
.form-group input[type="range"]:focus { box-shadow: none; }
|
.form-group input[type="range"]:focus { box-shadow: none; }
|
||||||
.form-group input[type="checkbox"] { width: auto; max-width: none; accent-color: var(--accent); }
|
.form-group input[type="checkbox"] { width: auto; accent-color: var(--accent); }
|
||||||
|
|
||||||
.toggle-group {
|
.toggle-group {
|
||||||
display: flex; gap: 1px; padding: 3px;
|
display: flex; gap: 1px; padding: 3px;
|
||||||
@@ -392,26 +354,32 @@ select option { background: var(--bg-surface); color: var(--text); }
|
|||||||
.toggle-btn.active { background: var(--bg-raised); color: var(--text); }
|
.toggle-btn.active { background: var(--bg-raised); color: var(--text); }
|
||||||
.toggle-btn:hover { color: var(--text-2); }
|
.toggle-btn:hover { color: var(--text-2); }
|
||||||
|
|
||||||
.btn-primary { background: var(--accent); color: var(--text-on-color); border: none; }
|
|
||||||
.btn-primary:hover { background: var(--accent-hover); }
|
|
||||||
.btn-danger { background: var(--danger); color: var(--text-on-color); border: none; }
|
|
||||||
.btn-ghost { background: transparent; color: var(--text-2); border: 1px solid var(--border); }
|
.btn-ghost { background: transparent; color: var(--text-2); border: 1px solid var(--border); }
|
||||||
.btn-ghost:hover { background: var(--bg-hover); }
|
.btn-ghost:hover { background: var(--bg-hover); }
|
||||||
.btn-subtle { background: var(--accent-dim); color: var(--accent); border: none; }
|
.btn-subtle { background: var(--accent-dim); color: var(--accent); border: none; }
|
||||||
.btn-sm { padding: 5px 11px; font-size: 12px; border-radius: 6px; cursor: pointer; font-weight: 500; font-family: inherit; }
|
.btn-sm { padding: 5px 11px; font-size: 12px; border-radius: 6px; cursor: pointer; font-weight: 500; font-family: inherit; }
|
||||||
.btn-md { padding: 7px 15px; font-size: 13px; border-radius: 8px; cursor: pointer; font-weight: 500; font-family: inherit; }
|
.btn-md { padding: 7px 15px; font-size: 13px; border-radius: var(--radius); cursor: pointer; font-weight: 500; font-family: inherit; }
|
||||||
|
|
||||||
/* ── Badge ───────────────────────────────── */
|
/* ── Badge ───────────────────────────────── */
|
||||||
.badge {
|
.badge {
|
||||||
font-size: 10px; padding: 2px 7px; border-radius: 4px;
|
font-size: 10px; padding: 2px 8px; border-radius: 4px;
|
||||||
font-weight: 600; letter-spacing: 0.3px; white-space: nowrap;
|
font-weight: 600; letter-spacing: 0.3px; white-space: nowrap;
|
||||||
display: inline-flex; align-items: center;
|
display: inline-flex; align-items: center;
|
||||||
}
|
}
|
||||||
.badge-accent { background: var(--accent-dim); color: var(--accent); }
|
|
||||||
.badge-success { background: var(--success-dim); color: var(--success); }
|
/* Dim variants (colored text on tinted background) */
|
||||||
.badge-warning { background: var(--warning-dim); color: var(--warning); }
|
.badge-accent, .badge-models, .badge-team { background: var(--accent-dim); color: var(--accent-light); }
|
||||||
.badge-danger { background: var(--danger-dim); color: var(--danger); }
|
.badge-success, .badge-active, .badge-healthy { background: var(--success-dim); color: var(--success-light); }
|
||||||
.badge-muted { background: var(--bg-raised); color: var(--text-3); }
|
.badge-warning, .badge-degraded { background: var(--warning-dim); color: var(--warning-light); }
|
||||||
|
.badge-danger, .badge-disabled, .badge-unhealthy { background: var(--danger-dim); color: var(--danger-light); }
|
||||||
|
.badge-muted, .badge-unknown, .badge-provider-type, .badge-global, .badge-user { background: var(--bg-raised); color: var(--text-3); }
|
||||||
|
.badge-purple, .badge-service { background: var(--purple-dim); color: var(--purple); }
|
||||||
|
|
||||||
|
/* Solid variants (white text on colored background) */
|
||||||
|
.badge-admin { background: var(--accent); color: var(--text-on-color); }
|
||||||
|
.badge-inactive { background: var(--danger); color: var(--text-on-color); }
|
||||||
|
.badge-pending { background: var(--warning); color: var(--text-on-warning); }
|
||||||
|
.badge-private { background: var(--purple); color: var(--text-on-color); }
|
||||||
|
|
||||||
/* ── Status Dot ──────────────────────────── */
|
/* ── Status Dot ──────────────────────────── */
|
||||||
.status-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
|
.status-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
|
||||||
@@ -429,4 +397,10 @@ select option { background: var(--bg-surface); color: var(--text); }
|
|||||||
}
|
}
|
||||||
.icon-btn:hover { background: var(--bg-hover); color: var(--text-2); }
|
.icon-btn:hover { background: var(--bg-hover); color: var(--text-2); }
|
||||||
.icon-btn.active { color: var(--accent); }
|
.icon-btn.active { color: var(--accent); }
|
||||||
|
.icon-btn-danger:hover { color: var(--danger); }
|
||||||
|
|
||||||
|
/* ── Utility States ─────────────────────── */
|
||||||
|
.loading { color: var(--text-3); font-size: 13px; padding: 0.5rem; }
|
||||||
|
.error-hint { color: var(--danger); font-size: 13px; padding: 0.5rem; }
|
||||||
|
.empty-hint { color: var(--text-3); font-size: 13px; text-align: center; padding: 1rem; }
|
||||||
|
|
||||||
|
|||||||
@@ -122,7 +122,7 @@
|
|||||||
.project-panel-label { font-size: 12px; font-weight: 600; color: var(--text-2); text-transform: uppercase; letter-spacing: 0.5px; }
|
.project-panel-label { font-size: 12px; font-weight: 600; color: var(--text-2); text-transform: uppercase; letter-spacing: 0.5px; }
|
||||||
.project-panel-textarea {
|
.project-panel-textarea {
|
||||||
width: 100%; resize: vertical; min-height: 80px; padding: 8px;
|
width: 100%; resize: vertical; min-height: 80px; padding: 8px;
|
||||||
font-family: var(--font-mono, monospace); font-size: 13px;
|
font-family: var(--mono); font-size: 13px;
|
||||||
background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius);
|
background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
@@ -429,7 +429,7 @@
|
|||||||
/* ── Bar Chart ───────────────────────────── */
|
/* ── Bar Chart ───────────────────────────── */
|
||||||
.bar-chart { display: flex; align-items: flex-end; gap: 8px; height: 140px; }
|
.bar-chart { display: flex; align-items: flex-end; gap: 8px; height: 140px; }
|
||||||
.bar-chart-col { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 6px; }
|
.bar-chart-col { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 6px; }
|
||||||
.bar-chart-val { font-size: 10px; color: var(--text-3); font-family: var(--font-mono, monospace); }
|
.bar-chart-val { font-size: 10px; color: var(--text-3); font-family: var(--mono); }
|
||||||
.bar-chart-track { width: 100%; background: var(--accent-dim); border-radius: 4px; position: relative; height: 100px; }
|
.bar-chart-track { width: 100%; background: var(--accent-dim); border-radius: 4px; position: relative; height: 100px; }
|
||||||
.bar-chart-fill { position: absolute; bottom: 0; left: 0; right: 0; background: var(--accent); border-radius: 4px; transition: height 0.3s ease; }
|
.bar-chart-fill { position: absolute; bottom: 0; left: 0; right: 0; background: var(--accent); border-radius: 4px; transition: height 0.3s ease; }
|
||||||
.bar-chart-label { font-size: 10px; color: var(--text-3); }
|
.bar-chart-label { font-size: 10px; color: var(--text-3); }
|
||||||
@@ -484,18 +484,8 @@
|
|||||||
}
|
}
|
||||||
.admin-user-name { font-weight: 600; font-size: 13px; }
|
.admin-user-name { font-weight: 600; font-size: 13px; }
|
||||||
.admin-user-handle { font-size: 11px; color: var(--text-3); margin-top: 1px; }
|
.admin-user-handle { font-size: 11px; color: var(--text-3); margin-top: 1px; }
|
||||||
.admin-user-time { color: var(--text-3); font-size: 12px; font-family: var(--font-mono, monospace); }
|
.admin-user-time { color: var(--text-3); font-size: 12px; font-family: var(--mono); }
|
||||||
.admin-actions-cell { white-space: nowrap; text-align: right; }
|
.admin-actions-cell { white-space: nowrap; text-align: right; }
|
||||||
.btn-icon {
|
|
||||||
background: none; border: none; color: var(--text-3); cursor: pointer;
|
|
||||||
padding: 4px; font-size: 14px; border-radius: 4px;
|
|
||||||
transition: color var(--transition), background var(--transition);
|
|
||||||
}
|
|
||||||
.btn-icon:hover { color: var(--text); background: var(--bg-hover); }
|
|
||||||
.btn-icon-danger:hover { color: var(--danger); }
|
|
||||||
.badge-service { background: var(--purple-dim); color: var(--purple); font-size: 10px; padding: 1px 8px; border-radius: 4px; }
|
|
||||||
.badge-disabled { background: var(--danger-dim); color: var(--danger-light); font-size: 10px; padding: 1px 8px; border-radius: 4px; }
|
|
||||||
.badge-active { background: var(--success-dim); color: var(--success-light); font-size: 10px; padding: 1px 8px; border-radius: 4px; }
|
|
||||||
|
|
||||||
/* ── Provider Cards ──────────────────────── */
|
/* ── Provider Cards ──────────────────────── */
|
||||||
.provider-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px; }
|
.provider-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px; }
|
||||||
@@ -507,12 +497,6 @@
|
|||||||
.provider-card-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 10px; }
|
.provider-card-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 10px; }
|
||||||
.provider-card-name { font-weight: 600; font-size: 14px; }
|
.provider-card-name { font-weight: 600; font-size: 14px; }
|
||||||
.provider-card-meta { display: flex; gap: 6px; align-items: center; margin-top: 4px; flex-wrap: wrap; }
|
.provider-card-meta { display: flex; gap: 6px; align-items: center; margin-top: 4px; flex-wrap: wrap; }
|
||||||
.badge-healthy { background: var(--success-dim); color: var(--success-light); font-size: 10px; padding: 2px 8px; border-radius: 4px; font-weight: 500; }
|
|
||||||
.badge-degraded { background: var(--warning-dim); color: var(--warning-light); font-size: 10px; padding: 2px 8px; border-radius: 4px; font-weight: 500; }
|
|
||||||
.badge-unhealthy { background: var(--danger-dim); color: var(--danger-light); font-size: 10px; padding: 2px 8px; border-radius: 4px; font-weight: 500; }
|
|
||||||
.badge-unknown { background: var(--bg-raised); color: var(--text-3); font-size: 10px; padding: 2px 8px; border-radius: 4px; }
|
|
||||||
.badge-models { background: var(--accent-dim); color: var(--accent-light); font-size: 10px; padding: 2px 8px; border-radius: 4px; }
|
|
||||||
.badge-provider-type { background: var(--bg-raised); color: var(--text-3); font-size: 10px; padding: 2px 8px; border-radius: 4px; }
|
|
||||||
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
@@ -775,3 +759,13 @@
|
|||||||
|
|
||||||
/* Indented chat items inside folders */
|
/* Indented chat items inside folders */
|
||||||
.chat-item.indented { padding-left: 28px; }
|
.chat-item.indented { padding-left: 28px; }
|
||||||
|
|
||||||
|
/* ── Notes Surface ─────────────────────── */
|
||||||
|
.surface-notes { display: flex; flex-direction: column; height: 100%; overflow: hidden; }
|
||||||
|
.surface-notes-body { flex: 1; display: flex; min-height: 0; overflow: hidden; }
|
||||||
|
.surface-notes-main { flex: 1; display: flex; flex-direction: column; min-width: 0; overflow: hidden; }
|
||||||
|
.surface-notes-assist {
|
||||||
|
width: 400px; border-left: 1px solid var(--border);
|
||||||
|
display: flex; flex-direction: column; overflow: hidden;
|
||||||
|
}
|
||||||
|
@media (max-width: 768px) { .surface-notes-assist { display: none; } }
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tool-grants-item:hover {
|
.tool-grants-item:hover {
|
||||||
background: var(--bg-tertiary, #1e1e22);
|
background: var(--bg-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-grants-item input[type="checkbox"] {
|
.tool-grants-item input[type="checkbox"] {
|
||||||
|
|||||||
@@ -474,13 +474,6 @@ function _initAdminListeners() {
|
|||||||
document.querySelectorAll('.admin-cat').forEach(btn => {
|
document.querySelectorAll('.admin-cat').forEach(btn => {
|
||||||
btn.addEventListener('click', () => UI.switchAdminCategory(btn.dataset.cat));
|
btn.addEventListener('click', () => UI.switchAdminCategory(btn.dataset.cat));
|
||||||
});
|
});
|
||||||
// Esc closes admin panel
|
|
||||||
document.addEventListener('keydown', e => {
|
|
||||||
if (e.key === 'Escape' && document.getElementById('adminPanel')?.style.display === 'flex') {
|
|
||||||
UI.closeAdmin();
|
|
||||||
e.stopPropagation();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
document.getElementById('adminSaveSettings')?.addEventListener('click', handleSaveAdminSettings);
|
document.getElementById('adminSaveSettings')?.addEventListener('click', handleSaveAdminSettings);
|
||||||
|
|
||||||
// Admin — users
|
// Admin — users
|
||||||
|
|||||||
@@ -120,22 +120,22 @@ async function _loadSurfaceList() {
|
|||||||
row.innerHTML =
|
row.innerHTML =
|
||||||
'<div class="admin-surface-info">' +
|
'<div class="admin-surface-info">' +
|
||||||
'<div class="admin-surface-title">' +
|
'<div class="admin-surface-title">' +
|
||||||
'<span class="admin-surface-name">' + _surfEsc(s.title) + '</span>' +
|
'<span class="admin-surface-name">' + esc(s.title) + '</span>' +
|
||||||
'<span class="admin-surface-badge admin-surface-badge--' + s.source + '">' + sourceLabel + '</span>' +
|
'<span class="admin-surface-badge admin-surface-badge--' + s.source + '">' + sourceLabel + '</span>' +
|
||||||
(isProtected ? '<span class="admin-surface-badge admin-surface-badge--required">Required</span>' : '') +
|
(isProtected ? '<span class="admin-surface-badge admin-surface-badge--required">Required</span>' : '') +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'<div class="admin-surface-meta">' +
|
'<div class="admin-surface-meta">' +
|
||||||
'<span class="admin-surface-id">' + _surfEsc(s.id) + '</span>' +
|
'<span class="admin-surface-id">' + esc(s.id) + '</span>' +
|
||||||
(route ? '<span class="admin-surface-route">' + _surfEsc(route) + '</span>' : '') +
|
(route ? '<span class="admin-surface-route">' + esc(route) + '</span>' : '') +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'<div class="admin-surface-actions">' +
|
'<div class="admin-surface-actions">' +
|
||||||
'<label class="admin-surface-toggle' + (isProtected ? ' admin-surface-toggle--locked' : '') + '">' +
|
'<label class="admin-surface-toggle' + (isProtected ? ' admin-surface-toggle--locked' : '') + '">' +
|
||||||
'<input type="checkbox"' + (s.enabled ? ' checked' : '') + (isProtected ? ' disabled' : '') +
|
'<input type="checkbox"' + (s.enabled ? ' checked' : '') + (isProtected ? ' disabled' : '') +
|
||||||
' data-surface-id="' + _surfEsc(s.id) + '">' +
|
' data-surface-id="' + esc(s.id) + '">' +
|
||||||
'<span class="admin-surface-toggle-label">' + (s.enabled ? 'Enabled' : 'Disabled') + '</span>' +
|
'<span class="admin-surface-toggle-label">' + (s.enabled ? 'Enabled' : 'Disabled') + '</span>' +
|
||||||
'</label>' +
|
'</label>' +
|
||||||
(s.source === 'extension' ? '<button class="btn-small btn-danger admin-surface-uninstall" data-surface-id="' + _surfEsc(s.id) + '">Uninstall</button>' : '') +
|
(s.source === 'extension' ? '<button class="btn-small btn-danger admin-surface-uninstall" data-surface-id="' + esc(s.id) + '">Uninstall</button>' : '') +
|
||||||
'</div>';
|
'</div>';
|
||||||
|
|
||||||
// Toggle handler
|
// Toggle handler
|
||||||
@@ -187,12 +187,7 @@ async function _loadSurfaceList() {
|
|||||||
listEl.appendChild(row);
|
listEl.appendChild(row);
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
listEl.innerHTML = '<div style="padding:12px;font-size:12px;color:var(--danger);">Failed to load: ' + _surfEsc(e.message) + '</div>';
|
listEl.innerHTML = '<div style="padding:12px;font-size:12px;color:var(--danger);">Failed to load: ' + esc(e.message) + '</div>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function _surfEsc(s) {
|
|
||||||
var d = document.createElement('div');
|
|
||||||
d.textContent = s || '';
|
|
||||||
return d.innerHTML;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,12 +6,11 @@
|
|||||||
// ChatPane.primary is the backward-compat bridge.
|
// ChatPane.primary is the backward-compat bridge.
|
||||||
|
|
||||||
const ChatPane = {
|
const ChatPane = {
|
||||||
primary: null,
|
...createComponentRegistry('ChatPane'),
|
||||||
_instances: new Map(),
|
|
||||||
|
|
||||||
create(opts) {
|
create(opts) {
|
||||||
const id = opts.id || 'pane-' + Date.now() + '-' + Math.random().toString(36).slice(2, 6);
|
const id = opts.id || 'pane-' + Date.now() + '-' + Math.random().toString(36).slice(2, 6);
|
||||||
const instance = {
|
const instance = componentMixin({
|
||||||
id,
|
id,
|
||||||
messagesEl: opts.messagesEl,
|
messagesEl: opts.messagesEl,
|
||||||
inputEl: opts.inputEl,
|
inputEl: opts.inputEl,
|
||||||
@@ -22,7 +21,6 @@ const ChatPane = {
|
|||||||
standalone: opts.standalone || false,
|
standalone: opts.standalone || false,
|
||||||
_cmView: null,
|
_cmView: null,
|
||||||
_typing: null,
|
_typing: null,
|
||||||
_listeners: [],
|
|
||||||
|
|
||||||
renderMessages(messages) {
|
renderMessages(messages) {
|
||||||
if (!this.messagesEl) return;
|
if (!this.messagesEl) return;
|
||||||
@@ -34,7 +32,7 @@ const ChatPane = {
|
|||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.className = 'chat-msg chat-msg--' + m.role;
|
div.className = 'chat-msg chat-msg--' + m.role;
|
||||||
div.innerHTML = typeof formatMessage === 'function'
|
div.innerHTML = typeof formatMessage === 'function'
|
||||||
? formatMessage(m) : _cpEsc(m.content || '');
|
? formatMessage(m) : esc(m.content || '');
|
||||||
this.messagesEl.appendChild(div);
|
this.messagesEl.appendChild(div);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -127,28 +125,16 @@ const ChatPane = {
|
|||||||
else { const ta = this.inputEl && this.inputEl.querySelector('textarea'); if (ta) ta.focus(); }
|
else { const ta = this.inputEl && this.inputEl.querySelector('textarea'); if (ta) ta.focus(); }
|
||||||
},
|
},
|
||||||
|
|
||||||
on(el, event, handler) {
|
_cleanup() {
|
||||||
if (!el) return;
|
|
||||||
el.addEventListener(event, handler);
|
|
||||||
this._listeners.push({ el, event, handler });
|
|
||||||
},
|
|
||||||
|
|
||||||
destroy() {
|
|
||||||
this._listeners.forEach(({ el, event, handler }) => el.removeEventListener(event, handler));
|
|
||||||
this._listeners = [];
|
|
||||||
if (this._cmView) { this._cmView.destroy(); this._cmView = null; }
|
if (this._cmView) { this._cmView.destroy(); this._cmView = null; }
|
||||||
this.removeTyping();
|
this.removeTyping();
|
||||||
ChatPane._instances.delete(this.id);
|
|
||||||
if (ChatPane.primary === this) ChatPane.primary = null;
|
|
||||||
},
|
},
|
||||||
};
|
}, ChatPane);
|
||||||
|
|
||||||
ChatPane._instances.set(id, instance);
|
ChatPane._register(id, instance);
|
||||||
return instance;
|
return instance;
|
||||||
},
|
},
|
||||||
|
|
||||||
get(id) { return this._instances.get(id) || null; },
|
|
||||||
|
|
||||||
forChannel(channelId) {
|
forChannel(channelId) {
|
||||||
for (const [, inst] of this._instances) {
|
for (const [, inst] of this._instances) {
|
||||||
if (inst.channelId === channelId) return inst;
|
if (inst.channelId === channelId) return inst;
|
||||||
@@ -159,4 +145,3 @@ const ChatPane = {
|
|||||||
active() { return this.primary; },
|
active() { return this.primary; },
|
||||||
};
|
};
|
||||||
|
|
||||||
function _cpEsc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
|
|
||||||
|
|||||||
@@ -19,11 +19,11 @@
|
|||||||
// editor.destroy();
|
// editor.destroy();
|
||||||
|
|
||||||
const CodeEditor = {
|
const CodeEditor = {
|
||||||
_instances: new Map(),
|
...createComponentRegistry('CodeEditor'),
|
||||||
|
|
||||||
create(opts) {
|
create(opts) {
|
||||||
const pfx = opts.id || 'editor';
|
const pfx = opts.id || 'editor';
|
||||||
const instance = {
|
const instance = componentMixin({
|
||||||
id: pfx,
|
id: pfx,
|
||||||
tabsEl: document.getElementById(pfx + 'EditorTabs'),
|
tabsEl: document.getElementById(pfx + 'EditorTabs'),
|
||||||
contentEl: document.getElementById(pfx + 'EditorContent'),
|
contentEl: document.getElementById(pfx + 'EditorContent'),
|
||||||
@@ -35,9 +35,8 @@ const CodeEditor = {
|
|||||||
workspaceId: opts.workspaceId || null,
|
workspaceId: opts.workspaceId || null,
|
||||||
onSave: opts.onSave || null,
|
onSave: opts.onSave || null,
|
||||||
onActivate: opts.onActivate || null,
|
onActivate: opts.onActivate || null,
|
||||||
_openFiles: new Map(), // path → { tab, editorWrap, editor, content, modified, language }
|
_openFiles: new Map(),
|
||||||
_activeFile: null,
|
_activeFile: null,
|
||||||
_listeners: [],
|
|
||||||
|
|
||||||
// ── Public API ──────────────────────
|
// ── Public API ──────────────────────
|
||||||
|
|
||||||
@@ -69,7 +68,7 @@ const CodeEditor = {
|
|||||||
const fileName = path.split('/').pop();
|
const fileName = path.split('/').pop();
|
||||||
tab.innerHTML =
|
tab.innerHTML =
|
||||||
'<span class="code-editor-tab-icon">' + _ceFileIcon(fileName) + '</span>' +
|
'<span class="code-editor-tab-icon">' + _ceFileIcon(fileName) + '</span>' +
|
||||||
'<span class="code-editor-tab-name">' + _ceEsc(fileName) + '</span>' +
|
'<span class="code-editor-tab-name">' + esc(fileName) + '</span>' +
|
||||||
'<span class="code-editor-tab-modified" style="display:none">●</span>' +
|
'<span class="code-editor-tab-modified" style="display:none">●</span>' +
|
||||||
'<button class="code-editor-tab-close" title="Close">✕</button>';
|
'<button class="code-editor-tab-close" title="Close">✕</button>';
|
||||||
|
|
||||||
@@ -235,10 +234,7 @@ const CodeEditor = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── Lifecycle ───────────────────────
|
_cleanup() {
|
||||||
|
|
||||||
destroy() {
|
|
||||||
// Destroy all CM6 instances
|
|
||||||
for (const [, f] of this._openFiles) {
|
for (const [, f] of this._openFiles) {
|
||||||
if (f.editor?.destroy) f.editor.destroy();
|
if (f.editor?.destroy) f.editor.destroy();
|
||||||
f.tab.remove();
|
f.tab.remove();
|
||||||
@@ -246,9 +242,6 @@ const CodeEditor = {
|
|||||||
}
|
}
|
||||||
this._openFiles.clear();
|
this._openFiles.clear();
|
||||||
this._activeFile = null;
|
this._activeFile = null;
|
||||||
this._listeners.forEach(({ el, event, handler }) => el.removeEventListener(event, handler));
|
|
||||||
this._listeners = [];
|
|
||||||
CodeEditor._instances.delete(this.id);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── Internal ────────────────────────
|
// ── Internal ────────────────────────
|
||||||
@@ -288,23 +281,15 @@ const CodeEditor = {
|
|||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
},
|
},
|
||||||
|
|
||||||
_on(el, event, handler) {
|
}, CodeEditor);
|
||||||
if (!el) return;
|
|
||||||
el.addEventListener(event, handler);
|
|
||||||
this._listeners.push({ el, event, handler });
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
CodeEditor._instances.set(pfx, instance);
|
CodeEditor._register(pfx, instance);
|
||||||
return instance;
|
return instance;
|
||||||
},
|
},
|
||||||
|
|
||||||
get(id) { return this._instances.get(id) || null; },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Helpers ─────────────────────────────────
|
// ── Helpers ─────────────────────────────────
|
||||||
|
|
||||||
function _ceEsc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
|
|
||||||
|
|
||||||
function _ceFileIcon(name) {
|
function _ceFileIcon(name) {
|
||||||
const ext = name.split('.').pop()?.toLowerCase();
|
const ext = name.split('.').pop()?.toLowerCase();
|
||||||
|
|||||||
@@ -603,7 +603,7 @@ function copyDebugLog() {
|
|||||||
const text = DebugLog.exportText();
|
const text = DebugLog.exportText();
|
||||||
navigator.clipboard.writeText(text)
|
navigator.clipboard.writeText(text)
|
||||||
.then(() => { if (typeof showToast === 'function') showToast('📋 Debug log copied', 'success'); })
|
.then(() => { if (typeof showToast === 'function') showToast('📋 Debug log copied', 'success'); })
|
||||||
.catch(() => { alert('Failed to copy'); });
|
.catch(() => { UI.toast('Copy failed', 'error'); });
|
||||||
}
|
}
|
||||||
|
|
||||||
function exportDebugLog() {
|
function exportDebugLog() {
|
||||||
|
|||||||
@@ -162,8 +162,8 @@
|
|||||||
const item = document.createElement('button');
|
const item = document.createElement('button');
|
||||||
item.className = 'editor-bootstrap-ws-item';
|
item.className = 'editor-bootstrap-ws-item';
|
||||||
item.innerHTML =
|
item.innerHTML =
|
||||||
'<span class="editor-bootstrap-ws-name">' + _edEsc(ws.name || ws.id?.slice(0, 8)) + '</span>' +
|
'<span class="editor-bootstrap-ws-name">' + esc(ws.name || ws.id?.slice(0, 8)) + '</span>' +
|
||||||
'<span class="editor-bootstrap-ws-date">' + _edEsc(ws.created_at ? new Date(ws.created_at).toLocaleDateString() : '') + '</span>';
|
'<span class="editor-bootstrap-ws-date">' + esc(ws.created_at ? new Date(ws.created_at).toLocaleDateString() : '') + '</span>';
|
||||||
item.addEventListener('click', () => {
|
item.addEventListener('click', () => {
|
||||||
window.location.href = (window.__BASE__ || '') + '/editor/' + ws.id;
|
window.location.href = (window.__BASE__ || '') + '/editor/' + ws.id;
|
||||||
});
|
});
|
||||||
@@ -336,7 +336,6 @@
|
|||||||
|
|
||||||
// ── Helpers ──────────────────────────────
|
// ── Helpers ──────────────────────────────
|
||||||
|
|
||||||
function _edEsc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
|
|
||||||
|
|
||||||
// ── Assist Chat (channel-based, standalone) ──
|
// ── Assist Chat (channel-based, standalone) ──
|
||||||
// Full chat pane for the editor: model selector, chat history,
|
// Full chat pane for the editor: model selector, chat history,
|
||||||
@@ -616,7 +615,7 @@
|
|||||||
function _appendSystemMsg(text) {
|
function _appendSystemMsg(text) {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.className = 'chat-msg chat-msg--system';
|
div.className = 'chat-msg chat-msg--system';
|
||||||
div.innerHTML = '<div class="chat-msg__content" style="color:var(--danger,#f44336);font-size:12px;">' + _edEsc(text) + '</div>';
|
div.innerHTML = '<div class="chat-msg__content" style="color:var(--danger,#f44336);font-size:12px;">' + esc(text) + '</div>';
|
||||||
chatPane.messagesEl?.appendChild(div);
|
chatPane.messagesEl?.appendChild(div);
|
||||||
chatPane.scrollToBottom();
|
chatPane.scrollToBottom();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,11 +18,11 @@
|
|||||||
// tree.destroy();
|
// tree.destroy();
|
||||||
|
|
||||||
const FileTree = {
|
const FileTree = {
|
||||||
_instances: new Map(),
|
...createComponentRegistry('FileTree'),
|
||||||
|
|
||||||
create(opts) {
|
create(opts) {
|
||||||
const pfx = opts.id || 'editor';
|
const pfx = opts.id || 'editor';
|
||||||
const instance = {
|
const instance = componentMixin({
|
||||||
id: pfx,
|
id: pfx,
|
||||||
itemsEl: document.getElementById(pfx + 'TreeItems'),
|
itemsEl: document.getElementById(pfx + 'TreeItems'),
|
||||||
newFileEl: document.getElementById(pfx + 'TreeNewFile'),
|
newFileEl: document.getElementById(pfx + 'TreeNewFile'),
|
||||||
@@ -34,7 +34,6 @@ const FileTree = {
|
|||||||
_expandedDirs: new Set(['']),
|
_expandedDirs: new Set(['']),
|
||||||
_gitStatusMap: new Map(),
|
_gitStatusMap: new Map(),
|
||||||
_activeFile: null,
|
_activeFile: null,
|
||||||
_listeners: [],
|
|
||||||
|
|
||||||
// ── Public API ──────────────────────
|
// ── Public API ──────────────────────
|
||||||
|
|
||||||
@@ -166,7 +165,7 @@ const FileTree = {
|
|||||||
row.innerHTML =
|
row.innerHTML =
|
||||||
'<span class="file-tree-arrow ' + (expanded ? 'expanded' : '') + '">' + (expanded ? '▾' : '▸') + '</span>' +
|
'<span class="file-tree-arrow ' + (expanded ? 'expanded' : '') + '">' + (expanded ? '▾' : '▸') + '</span>' +
|
||||||
'<span class="file-tree-icon">📁</span>' +
|
'<span class="file-tree-icon">📁</span>' +
|
||||||
'<span class="file-tree-name">' + _ftEsc(node.name) + '</span>';
|
'<span class="file-tree-name">' + esc(node.name) + '</span>';
|
||||||
row.addEventListener('click', () => {
|
row.addEventListener('click', () => {
|
||||||
if (self._expandedDirs.has(node.path)) {
|
if (self._expandedDirs.has(node.path)) {
|
||||||
self._expandedDirs.delete(node.path);
|
self._expandedDirs.delete(node.path);
|
||||||
@@ -182,7 +181,7 @@ const FileTree = {
|
|||||||
} else {
|
} else {
|
||||||
row.innerHTML =
|
row.innerHTML =
|
||||||
'<span class="file-tree-icon">' + _ftFileIcon(node.name) + '</span>' +
|
'<span class="file-tree-icon">' + _ftFileIcon(node.name) + '</span>' +
|
||||||
'<span class="file-tree-name">' + _ftEsc(node.name) + '</span>';
|
'<span class="file-tree-name">' + esc(node.name) + '</span>';
|
||||||
row.addEventListener('click', () => {
|
row.addEventListener('click', () => {
|
||||||
if (self.onSelect) self.onSelect(node.path);
|
if (self.onSelect) self.onSelect(node.path);
|
||||||
});
|
});
|
||||||
@@ -233,34 +232,20 @@ const FileTree = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── Lifecycle ───────────────────────
|
_cleanup() {
|
||||||
|
|
||||||
destroy() {
|
|
||||||
this._listeners.forEach(({ el, event, handler }) => el.removeEventListener(event, handler));
|
|
||||||
this._listeners = [];
|
|
||||||
this._treeData = [];
|
this._treeData = [];
|
||||||
this._expandedDirs = new Set(['']);
|
this._expandedDirs = new Set(['']);
|
||||||
this._gitStatusMap.clear();
|
this._gitStatusMap.clear();
|
||||||
FileTree._instances.delete(this.id);
|
|
||||||
},
|
},
|
||||||
|
}, FileTree);
|
||||||
|
|
||||||
_on(el, event, handler) {
|
FileTree._register(pfx, instance);
|
||||||
if (!el) return;
|
|
||||||
el.addEventListener(event, handler);
|
|
||||||
this._listeners.push({ el, event, handler });
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
FileTree._instances.set(pfx, instance);
|
|
||||||
return instance;
|
return instance;
|
||||||
},
|
},
|
||||||
|
|
||||||
get(id) { return this._instances.get(id) || null; },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Helpers ─────────────────────────────────
|
// ── Helpers ─────────────────────────────────
|
||||||
|
|
||||||
function _ftEsc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
|
|
||||||
|
|
||||||
function _ftFileIcon(name) {
|
function _ftFileIcon(name) {
|
||||||
const ext = name.split('.').pop()?.toLowerCase();
|
const ext = name.split('.').pop()?.toLowerCase();
|
||||||
|
|||||||
@@ -344,8 +344,8 @@ function _renderStrip() {
|
|||||||
return `<div class="att-chip ${statusClass}" data-local-id="${a.localId}">
|
return `<div class="att-chip ${statusClass}" data-local-id="${a.localId}">
|
||||||
${a.previewUrl ? `<img class="att-thumb" src="${a.previewUrl}" alt="">` : `<span class="att-icon">${icon}</span>`}
|
${a.previewUrl ? `<img class="att-thumb" src="${a.previewUrl}" alt="">` : `<span class="att-icon">${icon}</span>`}
|
||||||
<span class="att-info">
|
<span class="att-info">
|
||||||
<span class="att-name" title="${_esc(a.filename)}">${_esc(name)}</span>
|
<span class="att-name" title="${esc(a.filename)}">${esc(name)}</span>
|
||||||
<span class="att-meta">${_esc(size)}${label ? ' · ' + label : ''}</span>
|
<span class="att-meta">${esc(size)}${label ? ' · ' + label : ''}</span>
|
||||||
</span>
|
</span>
|
||||||
<button class="att-remove" onclick="unstageFile('${a.localId}')" title="Remove">✕</button>
|
<button class="att-remove" onclick="unstageFile('${a.localId}')" title="Remove">✕</button>
|
||||||
</div>`;
|
</div>`;
|
||||||
@@ -363,12 +363,6 @@ function _truncFilename(name, max) {
|
|||||||
return name.slice(0, max - 1) + '…';
|
return name.slice(0, max - 1) + '…';
|
||||||
}
|
}
|
||||||
|
|
||||||
function _esc(s) {
|
|
||||||
if (!s) return '';
|
|
||||||
const d = document.createElement('div');
|
|
||||||
d.textContent = s;
|
|
||||||
return d.innerHTML;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// ── Channel File Loading ──────────────
|
// ── Channel File Loading ──────────────
|
||||||
@@ -426,8 +420,8 @@ function renderMessageFiles(msgId) {
|
|||||||
const visionHint = !hasVision
|
const visionHint = !hasVision
|
||||||
? '<span class="att-vision-hint" title="Current model does not support images">👁️🗨️</span>'
|
? '<span class="att-vision-hint" title="Current model does not support images">👁️🗨️</span>'
|
||||||
: '';
|
: '';
|
||||||
return `<div class="msg-att msg-att-image" onclick="openLightbox('${_esc(att.id)}')">
|
return `<div class="msg-att msg-att-image" onclick="openLightbox('${esc(att.id)}')">
|
||||||
<img class="msg-att-img" data-att-id="${_esc(att.id)}" alt="${_esc(att.filename)}"
|
<img class="msg-att-img" data-att-id="${esc(att.id)}" alt="${esc(att.filename)}"
|
||||||
style="max-width:${w}px" loading="lazy">
|
style="max-width:${w}px" loading="lazy">
|
||||||
${visionHint}
|
${visionHint}
|
||||||
</div>`;
|
</div>`;
|
||||||
@@ -436,10 +430,10 @@ function renderMessageFiles(msgId) {
|
|||||||
// Document chip
|
// Document chip
|
||||||
const icon = _docIcon(att.content_type);
|
const icon = _docIcon(att.content_type);
|
||||||
const size = formatFileSize(att.size_bytes);
|
const size = formatFileSize(att.size_bytes);
|
||||||
return `<a class="msg-att msg-att-doc" href="#" onclick="downloadFile('${_esc(att.id)}','${_esc(att.filename)}');return false">
|
return `<a class="msg-att msg-att-doc" href="#" onclick="downloadFile('${esc(att.id)}','${esc(att.filename)}');return false">
|
||||||
<span class="att-doc-icon">${icon}</span>
|
<span class="att-doc-icon">${icon}</span>
|
||||||
<span class="att-doc-info">
|
<span class="att-doc-info">
|
||||||
<span class="att-doc-name">${_esc(att.filename)}</span>
|
<span class="att-doc-name">${esc(att.filename)}</span>
|
||||||
<span class="att-doc-size">${size}</span>
|
<span class="att-doc-size">${size}</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="att-doc-dl">⬇</span>
|
<span class="att-doc-dl">⬇</span>
|
||||||
@@ -565,7 +559,7 @@ async function loadAdminStorage() {
|
|||||||
<div class="admin-storage-grid">
|
<div class="admin-storage-grid">
|
||||||
<div class="admin-storage-item">
|
<div class="admin-storage-item">
|
||||||
<span class="admin-storage-label">Backend</span>
|
<span class="admin-storage-label">Backend</span>
|
||||||
<span class="admin-storage-value">${_esc(status.backend || 'none')}</span>
|
<span class="admin-storage-value">${esc(status.backend || 'none')}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="admin-storage-item">
|
<div class="admin-storage-item">
|
||||||
<span class="admin-storage-label">Status</span>
|
<span class="admin-storage-label">Status</span>
|
||||||
@@ -581,15 +575,15 @@ async function loadAdminStorage() {
|
|||||||
</div>
|
</div>
|
||||||
${status.path ? `<div class="admin-storage-item">
|
${status.path ? `<div class="admin-storage-item">
|
||||||
<span class="admin-storage-label">Path</span>
|
<span class="admin-storage-label">Path</span>
|
||||||
<span class="admin-storage-value" style="font-family:monospace;font-size:12px">${_esc(status.path)}</span>
|
<span class="admin-storage-value" style="font-family:monospace;font-size:12px">${esc(status.path)}</span>
|
||||||
</div>` : ''}
|
</div>` : ''}
|
||||||
${status.endpoint ? `<div class="admin-storage-item">
|
${status.endpoint ? `<div class="admin-storage-item">
|
||||||
<span class="admin-storage-label">Endpoint</span>
|
<span class="admin-storage-label">Endpoint</span>
|
||||||
<span class="admin-storage-value" style="font-family:monospace;font-size:12px">${_esc(status.endpoint)}</span>
|
<span class="admin-storage-value" style="font-family:monospace;font-size:12px">${esc(status.endpoint)}</span>
|
||||||
</div>` : ''}
|
</div>` : ''}
|
||||||
${status.bucket ? `<div class="admin-storage-item">
|
${status.bucket ? `<div class="admin-storage-item">
|
||||||
<span class="admin-storage-label">Bucket</span>
|
<span class="admin-storage-label">Bucket</span>
|
||||||
<span class="admin-storage-value" style="font-family:monospace;font-size:12px">${_esc(status.bucket)}</span>
|
<span class="admin-storage-value" style="font-family:monospace;font-size:12px">${esc(status.bucket)}</span>
|
||||||
</div>` : ''}
|
</div>` : ''}
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
@@ -640,7 +634,7 @@ async function loadAdminStorage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
el.innerHTML = `<div class="empty-hint">Failed to load storage status: ${_esc(e.message)}</div>`;
|
el.innerHTML = `<div class="empty-hint">Failed to load storage status: ${esc(e.message)}</div>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,12 +29,12 @@ const KnowledgeUI = (() => {
|
|||||||
if (!popup) return;
|
if (!popup) return;
|
||||||
|
|
||||||
if (!App.activeId) {
|
if (!App.activeId) {
|
||||||
popup.innerHTML = '<div class="kb-popup-empty">Start a conversation first</div>';
|
popup.innerHTML = '<div class="empty-hint">Start a conversation first</div>';
|
||||||
popup.classList.add('open');
|
popup.classList.add('open');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
popup.innerHTML = '<div class="kb-popup-loading">Loading…</div>';
|
popup.innerHTML = '<div class="loading">Loading…</div>';
|
||||||
popup.classList.add('open');
|
popup.classList.add('open');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -46,7 +46,7 @@ const KnowledgeUI = (() => {
|
|||||||
_channelKBs = chResp.data || [];
|
_channelKBs = chResp.data || [];
|
||||||
_renderChannelPopup(popup);
|
_renderChannelPopup(popup);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
popup.innerHTML = '<div class="kb-popup-empty">Failed to load knowledge bases</div>';
|
popup.innerHTML = '<div class="empty-hint">Failed to load knowledge bases</div>';
|
||||||
console.warn('KB popup load failed:', e);
|
console.warn('KB popup load failed:', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,7 +54,7 @@ const KnowledgeUI = (() => {
|
|||||||
function _renderChannelPopup(popup) {
|
function _renderChannelPopup(popup) {
|
||||||
if (_allKBs.length === 0) {
|
if (_allKBs.length === 0) {
|
||||||
popup.innerHTML = `
|
popup.innerHTML = `
|
||||||
<div class="kb-popup-empty">
|
<div class="empty-hint">
|
||||||
No knowledge bases yet.
|
No knowledge bases yet.
|
||||||
<br><small>Create one in Settings → Knowledge Bases</small>
|
<br><small>Create one in Settings → Knowledge Bases</small>
|
||||||
</div>`;
|
</div>`;
|
||||||
@@ -73,7 +73,7 @@ const KnowledgeUI = (() => {
|
|||||||
<div class="kb-popup-item ${linked ? 'enabled' : ''}" data-kb-id="${kb.id}">
|
<div class="kb-popup-item ${linked ? 'enabled' : ''}" data-kb-id="${kb.id}">
|
||||||
<span class="kb-popup-check"></span>
|
<span class="kb-popup-check"></span>
|
||||||
<span class="kb-popup-label">
|
<span class="kb-popup-label">
|
||||||
<span class="kb-popup-name">${_esc(kb.name)}</span>
|
<span class="kb-popup-name">${esc(kb.name)}</span>
|
||||||
<span class="kb-popup-meta">${docs}${scope}${status}</span>
|
<span class="kb-popup-meta">${docs}${scope}${status}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>`;
|
</div>`;
|
||||||
@@ -130,7 +130,7 @@ const KnowledgeUI = (() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_panelCtx = { container: containerId, scope, teamId };
|
_panelCtx = { container: containerId, scope, teamId };
|
||||||
panel.innerHTML = '<div class="kb-manage-loading">Loading…</div>';
|
panel.innerHTML = '<div class="loading">Loading…</div>';
|
||||||
panel.style.display = '';
|
panel.style.display = '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -146,7 +146,7 @@ const KnowledgeUI = (() => {
|
|||||||
_renderManagePanel(panel);
|
_renderManagePanel(panel);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('[KB] openPanel error:', e);
|
console.warn('[KB] openPanel error:', e);
|
||||||
panel.innerHTML = '<div class="kb-manage-empty">Failed to load knowledge bases</div>';
|
panel.innerHTML = '<div class="empty-hint">Failed to load knowledge bases</div>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,7 +166,7 @@ const KnowledgeUI = (() => {
|
|||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
if (_allKBs.length === 0) {
|
if (_allKBs.length === 0) {
|
||||||
html += '<div class="kb-manage-empty">No knowledge bases yet. Create one to get started.</div>';
|
html += '<div class="empty-hint">No knowledge bases yet. Create one to get started.</div>';
|
||||||
} else {
|
} else {
|
||||||
// Summary stats
|
// Summary stats
|
||||||
const totalDocs = _allKBs.reduce((s, kb) => s + (kb.document_count || 0), 0);
|
const totalDocs = _allKBs.reduce((s, kb) => s + (kb.document_count || 0), 0);
|
||||||
@@ -184,12 +184,12 @@ const KnowledgeUI = (() => {
|
|||||||
html += `
|
html += `
|
||||||
<div class="kb-manage-item" data-kb-id="${kb.id}" data-grant-kb="${kb.id}">
|
<div class="kb-manage-item" data-kb-id="${kb.id}" data-grant-kb="${kb.id}">
|
||||||
<div class="kb-manage-info">
|
<div class="kb-manage-info">
|
||||||
<span class="kb-manage-name">${scope} ${_esc(kb.name)}</span>
|
<span class="kb-manage-name">${scope} ${esc(kb.name)}</span>
|
||||||
<span class="kb-manage-desc">${_esc(kb.description || '')}</span>
|
<span class="kb-manage-desc">${esc(kb.description || '')}</span>
|
||||||
<span class="kb-manage-stats">${docs} · ${chunks}${size}${status}</span>
|
<span class="kb-manage-stats">${docs} · ${chunks}${size}${status}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="kb-manage-actions">
|
<div class="kb-manage-actions">
|
||||||
${kb.scope !== 'personal' ? `<button class="btn-small" data-kb-grant="${kb.id}" data-kb-name="${_esc(kb.name)}" title="Access">🔒</button>` : ''}
|
${kb.scope !== 'personal' ? `<button class="btn-small" data-kb-grant="${kb.id}" data-kb-name="${esc(kb.name)}" title="Access">🔒</button>` : ''}
|
||||||
<button class="btn-small" data-kb-docs="${kb.id}" title="Documents">📄</button>
|
<button class="btn-small" data-kb-docs="${kb.id}" title="Documents">📄</button>
|
||||||
<button class="btn-small btn-danger" data-kb-del="${kb.id}" title="Delete">✕</button>
|
<button class="btn-small btn-danger" data-kb-del="${kb.id}" title="Delete">✕</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -273,7 +273,7 @@ const KnowledgeUI = (() => {
|
|||||||
close();
|
close();
|
||||||
_refreshCurrentPanel();
|
_refreshCurrentPanel();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert('Failed to create: ' + (e.message || e));
|
UI.toast('Failed to create: ' + (e.message || e), 'error');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -292,12 +292,12 @@ const KnowledgeUI = (() => {
|
|||||||
|
|
||||||
async function _deleteKB(kbId) {
|
async function _deleteKB(kbId) {
|
||||||
const kb = _allKBs.find(k => k.id === kbId);
|
const kb = _allKBs.find(k => k.id === kbId);
|
||||||
if (!confirm(`Delete "${kb?.name || kbId}" and all its documents?`)) return;
|
if (!await showConfirm(`Delete "${kb?.name || kbId}" and all its documents?`)) return;
|
||||||
try {
|
try {
|
||||||
await API.deleteKnowledgeBase(kbId);
|
await API.deleteKnowledgeBase(kbId);
|
||||||
_refreshCurrentPanel();
|
_refreshCurrentPanel();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert('Delete failed: ' + (e.message || e));
|
UI.toast('Delete failed: ' + (e.message || e), 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,7 +322,7 @@ const KnowledgeUI = (() => {
|
|||||||
const panel = document.getElementById(containerId);
|
const panel = document.getElementById(containerId);
|
||||||
if (!panel) return;
|
if (!panel) return;
|
||||||
|
|
||||||
panel.innerHTML = '<div class="kb-manage-loading">Loading documents…</div>';
|
panel.innerHTML = '<div class="loading">Loading documents…</div>';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [kbResp, docsResp] = await Promise.all([
|
const [kbResp, docsResp] = await Promise.all([
|
||||||
@@ -333,7 +333,7 @@ const KnowledgeUI = (() => {
|
|||||||
const docs = docsResp.data || docsResp || [];
|
const docs = docsResp.data || docsResp || [];
|
||||||
_renderDocumentsPanel(panel, kb, Array.isArray(docs) ? docs : []);
|
_renderDocumentsPanel(panel, kb, Array.isArray(docs) ? docs : []);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
panel.innerHTML = `<div class="kb-manage-empty">Failed to load documents: ${_esc(e.message || '')}</div>`;
|
panel.innerHTML = `<div class="empty-hint">Failed to load documents: ${esc(e.message || '')}</div>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,7 +341,7 @@ const KnowledgeUI = (() => {
|
|||||||
let html = `
|
let html = `
|
||||||
<div class="kb-manage-header">
|
<div class="kb-manage-header">
|
||||||
<button class="btn-small" id="kbDocsBack">← Back</button>
|
<button class="btn-small" id="kbDocsBack">← Back</button>
|
||||||
<h3>${_esc(kb.name)}</h3>
|
<h3>${esc(kb.name)}</h3>
|
||||||
<label class="btn-small btn-primary" id="kbUploadLabel" title="Upload document">
|
<label class="btn-small btn-primary" id="kbUploadLabel" title="Upload document">
|
||||||
+ Upload
|
+ Upload
|
||||||
<input type="file" id="kbFileInput" accept=".txt,.md,.csv,.html,.htm" style="display:none" multiple>
|
<input type="file" id="kbFileInput" accept=".txt,.md,.csv,.html,.htm" style="display:none" multiple>
|
||||||
@@ -349,7 +349,7 @@ const KnowledgeUI = (() => {
|
|||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
if (kb.description) {
|
if (kb.description) {
|
||||||
html += `<div class="kb-manage-desc-block">${_esc(kb.description)}</div>`;
|
html += `<div class="kb-manage-desc-block">${esc(kb.description)}</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stats line
|
// Stats line
|
||||||
@@ -360,18 +360,18 @@ const KnowledgeUI = (() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (docs.length === 0) {
|
if (docs.length === 0) {
|
||||||
html += '<div class="kb-manage-empty">No documents yet. Upload text, markdown, CSV, or HTML files.</div>';
|
html += '<div class="empty-hint">No documents yet. Upload text, markdown, CSV, or HTML files.</div>';
|
||||||
} else {
|
} else {
|
||||||
html += '<div class="kb-manage-list">';
|
html += '<div class="kb-manage-list">';
|
||||||
for (const doc of docs) {
|
for (const doc of docs) {
|
||||||
const statusIcon = _docStatusIcon(doc.status);
|
const statusIcon = _docStatusIcon(doc.status);
|
||||||
const size = _formatSize(doc.size_bytes);
|
const size = _formatSize(doc.size_bytes);
|
||||||
const chunks = doc.chunk_count > 0 ? ` · ${doc.chunk_count} chunks` : '';
|
const chunks = doc.chunk_count > 0 ? ` · ${doc.chunk_count} chunks` : '';
|
||||||
const errText = doc.error ? ` · ⚠ ${_esc(doc.error)}` : '';
|
const errText = doc.error ? ` · ⚠ ${esc(doc.error)}` : '';
|
||||||
html += `
|
html += `
|
||||||
<div class="kb-manage-item kb-doc-item" data-doc-id="${doc.id}" data-doc-status="${doc.status}">
|
<div class="kb-manage-item kb-doc-item" data-doc-id="${doc.id}" data-doc-status="${doc.status}">
|
||||||
<div class="kb-manage-info">
|
<div class="kb-manage-info">
|
||||||
<span class="kb-manage-name">${statusIcon} ${_esc(doc.filename)}</span>
|
<span class="kb-manage-name">${statusIcon} ${esc(doc.filename)}</span>
|
||||||
<span class="kb-manage-stats">${size}${chunks}${errText}</span>
|
<span class="kb-manage-stats">${size}${chunks}${errText}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="kb-manage-actions">
|
<div class="kb-manage-actions">
|
||||||
@@ -417,18 +417,18 @@ const KnowledgeUI = (() => {
|
|||||||
console.log(`📄 Uploaded ${file.name}, status: ${resp.status}`);
|
console.log(`📄 Uploaded ${file.name}, status: ${resp.status}`);
|
||||||
if (resp.id) _startPolling(kbId, resp.id);
|
if (resp.id) _startPolling(kbId, resp.id);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert(`Upload failed for ${file.name}: ${e.message || e}`);
|
UI.toast(`Upload failed for ${file.name}: ${e.message || e}`, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function _deleteDocument(kbId, docId) {
|
async function _deleteDocument(kbId, docId) {
|
||||||
if (!confirm('Delete this document and all its chunks?')) return;
|
if (!await showConfirm('Delete this document and all its chunks?')) return;
|
||||||
try {
|
try {
|
||||||
_stopPolling(docId);
|
_stopPolling(docId);
|
||||||
await API.deleteKBDocument(kbId, docId);
|
await API.deleteKBDocument(kbId, docId);
|
||||||
openDocumentsPanel(kbId);
|
openDocumentsPanel(kbId);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert('Delete failed: ' + (e.message || e));
|
UI.toast('Delete failed: ' + (e.message || e), 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -467,7 +467,7 @@ const KnowledgeUI = (() => {
|
|||||||
if (nameEl) {
|
if (nameEl) {
|
||||||
const icon = _docStatusIcon(status.status);
|
const icon = _docStatusIcon(status.status);
|
||||||
const filename = nameEl.textContent.replace(/^[^\s]+ /, '');
|
const filename = nameEl.textContent.replace(/^[^\s]+ /, '');
|
||||||
nameEl.innerHTML = `${icon} ${_esc(status.filename || filename)}`;
|
nameEl.innerHTML = `${icon} ${esc(status.filename || filename)}`;
|
||||||
}
|
}
|
||||||
const statsEl = item.querySelector('.kb-manage-stats');
|
const statsEl = item.querySelector('.kb-manage-stats');
|
||||||
if (statsEl && status.chunk_count > 0) {
|
if (statsEl && status.chunk_count > 0) {
|
||||||
@@ -538,12 +538,6 @@ const KnowledgeUI = (() => {
|
|||||||
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
||||||
}
|
}
|
||||||
|
|
||||||
function _esc(s) {
|
|
||||||
if (!s) return '';
|
|
||||||
const d = document.createElement('div');
|
|
||||||
d.textContent = s;
|
|
||||||
return d.innerHTML;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ═════════════════════════════════════════
|
// ═════════════════════════════════════════
|
||||||
// Public API
|
// Public API
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ const MemoryUI = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async deleteMemory(id) {
|
async deleteMemory(id) {
|
||||||
if (!confirm('Delete this memory permanently?')) return;
|
if (!await showConfirm('Delete this memory permanently?')) return;
|
||||||
try {
|
try {
|
||||||
await API.deleteMemory(id);
|
await API.deleteMemory(id);
|
||||||
UI.toast('Memory deleted', 'success');
|
UI.toast('Memory deleted', 'success');
|
||||||
@@ -213,7 +213,7 @@ const MemoryUI = {
|
|||||||
const data = await API.listMemories('pending_review', '');
|
const data = await API.listMemories('pending_review', '');
|
||||||
const ids = (data.memories || []).map(m => m.id);
|
const ids = (data.memories || []).map(m => m.id);
|
||||||
if (!ids.length) return UI.toast('No pending memories', 'info');
|
if (!ids.length) return UI.toast('No pending memories', 'info');
|
||||||
if (!confirm(`Approve all ${ids.length} pending memories?`)) return;
|
if (!await showConfirm(`Approve all ${ids.length} pending memories?`)) return;
|
||||||
await API.bulkApproveMemories(ids);
|
await API.bulkApproveMemories(ids);
|
||||||
UI.toast(`${ids.length} memories approved`, 'success');
|
UI.toast(`${ids.length} memories approved`, 'success');
|
||||||
this.openSettingsPanel();
|
this.openSettingsPanel();
|
||||||
@@ -250,7 +250,7 @@ const MemoryUI = {
|
|||||||
// Wire events
|
// Wire events
|
||||||
document.getElementById('adminBulkApproveBtn')?.addEventListener('click', async () => {
|
document.getElementById('adminBulkApproveBtn')?.addEventListener('click', async () => {
|
||||||
const ids = pending.map(m => m.id);
|
const ids = pending.map(m => m.id);
|
||||||
if (!confirm(`Approve all ${ids.length} pending memories?`)) return;
|
if (!await showConfirm(`Approve all ${ids.length} pending memories?`)) return;
|
||||||
try {
|
try {
|
||||||
await API.bulkApproveMemories(ids);
|
await API.bulkApproveMemories(ids);
|
||||||
UI.toast(`${ids.length} memories approved`, 'success');
|
UI.toast(`${ids.length} memories approved`, 'success');
|
||||||
|
|||||||
@@ -13,12 +13,11 @@
|
|||||||
// sel.destroy();
|
// sel.destroy();
|
||||||
|
|
||||||
const ModelSelector = {
|
const ModelSelector = {
|
||||||
primary: null,
|
...createComponentRegistry('ModelSelector'),
|
||||||
_instances: new Map(),
|
|
||||||
|
|
||||||
create(opts) {
|
create(opts) {
|
||||||
const pfx = opts.id || '';
|
const pfx = opts.id || '';
|
||||||
const instance = {
|
const instance = componentMixin({
|
||||||
id: pfx,
|
id: pfx,
|
||||||
dropdownEl: document.getElementById(pfx + 'modelDropdown'),
|
dropdownEl: document.getElementById(pfx + 'modelDropdown'),
|
||||||
btnEl: document.getElementById(pfx + 'modelDropdownBtn'),
|
btnEl: document.getElementById(pfx + 'modelDropdownBtn'),
|
||||||
@@ -28,7 +27,6 @@ const ModelSelector = {
|
|||||||
onChange: opts.onChange || null,
|
onChange: opts.onChange || null,
|
||||||
_value: '',
|
_value: '',
|
||||||
_models: [],
|
_models: [],
|
||||||
_listeners: [],
|
|
||||||
|
|
||||||
// ── Selection ───────────────────────
|
// ── Selection ───────────────────────
|
||||||
|
|
||||||
@@ -100,11 +98,11 @@ const ModelSelector = {
|
|||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.className = 'model-dropdown-item';
|
div.className = 'model-dropdown-item';
|
||||||
div.dataset.value = m.id;
|
div.dataset.value = m.id;
|
||||||
const avatar = m.personaAvatar ? '<img src="' + _msEsc(m.personaAvatar) + '" class="dropdown-avatar" alt="">' : '';
|
const avatar = m.personaAvatar ? '<img src="' + esc(m.personaAvatar) + '" class="dropdown-avatar" alt="">' : '';
|
||||||
const handle = m.isPersona && m.personaHandle ? '<span class="item-handle">@' + _msEsc(m.personaHandle) + '</span>' : '';
|
const handle = m.isPersona && m.personaHandle ? '<span class="item-handle">@' + esc(m.personaHandle) + '</span>' : '';
|
||||||
const teamBadge = m.source === 'team' && m.teamName ? '<span class="badge-team" style="font-size:9px;padding:0 4px">\uD83D\uDC65 ' + _msEsc(m.teamName) + '</span>' : '';
|
const teamBadge = m.source === 'team' && m.teamName ? '<span class="badge-team" style="font-size:9px;padding:0 4px">\uD83D\uDC65 ' + esc(m.teamName) + '</span>' : '';
|
||||||
const provider = m.provider ? '<span class="item-provider">' + _msEsc(m.provider) + '</span>' : '';
|
const provider = m.provider ? '<span class="item-provider">' + esc(m.provider) + '</span>' : '';
|
||||||
div.innerHTML = avatar + '<span class="item-label">' + _msEsc(m.name || m.id) + handle + '</span>' + teamBadge + provider;
|
div.innerHTML = avatar + '<span class="item-label">' + esc(m.name || m.id) + handle + '</span>' + teamBadge + provider;
|
||||||
div.addEventListener('click', () => {
|
div.addEventListener('click', () => {
|
||||||
self.select(m.id, (m.name || m.id) + (m.provider ? ' (' + m.provider + ')' : ''));
|
self.select(m.id, (m.name || m.id) + (m.provider ? ' (' + m.provider + ')' : ''));
|
||||||
self.close();
|
self.close();
|
||||||
@@ -163,29 +161,9 @@ const ModelSelector = {
|
|||||||
if (!e.target.closest('#' + pfx + 'modelDropdown')) this.close();
|
if (!e.target.closest('#' + pfx + 'modelDropdown')) this.close();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
}, ModelSelector);
|
||||||
|
|
||||||
// ── Lifecycle ───────────────────────
|
ModelSelector._register(pfx, instance);
|
||||||
|
|
||||||
destroy() {
|
|
||||||
this._listeners.forEach(({ el, event, handler }) => el.removeEventListener(event, handler));
|
|
||||||
this._listeners = [];
|
|
||||||
ModelSelector._instances.delete(this.id);
|
|
||||||
if (ModelSelector.primary === this) ModelSelector.primary = null;
|
|
||||||
},
|
|
||||||
|
|
||||||
_on(el, event, handler) {
|
|
||||||
if (!el) return;
|
|
||||||
el.addEventListener(event, handler);
|
|
||||||
this._listeners.push({ el, event, handler });
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
ModelSelector._instances.set(pfx, instance);
|
|
||||||
return instance;
|
return instance;
|
||||||
},
|
},
|
||||||
|
|
||||||
get(id) { return this._instances.get(id) || null; },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// HTML-escape for model selector content
|
|
||||||
function _msEsc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
|
|
||||||
|
|||||||
@@ -20,11 +20,11 @@
|
|||||||
// (editor pane, future notes-studio layout).
|
// (editor pane, future notes-studio layout).
|
||||||
|
|
||||||
const NoteEditor = {
|
const NoteEditor = {
|
||||||
_instances: new Map(),
|
...createComponentRegistry('NoteEditor'),
|
||||||
|
|
||||||
create(opts) {
|
create(opts) {
|
||||||
const pfx = opts.id || 'main';
|
const pfx = opts.id || 'main';
|
||||||
const instance = {
|
const instance = componentMixin({
|
||||||
id: pfx,
|
id: pfx,
|
||||||
rootEl: document.getElementById(pfx + 'NoteEditor'),
|
rootEl: document.getElementById(pfx + 'NoteEditor'),
|
||||||
listViewEl: document.getElementById(pfx + 'NotesListView'),
|
listViewEl: document.getElementById(pfx + 'NotesListView'),
|
||||||
@@ -51,7 +51,6 @@ const NoteEditor = {
|
|||||||
_currentNote: null,
|
_currentNote: null,
|
||||||
_sort: 'updated_desc',
|
_sort: 'updated_desc',
|
||||||
_cmEditor: null,
|
_cmEditor: null,
|
||||||
_listeners: [],
|
|
||||||
|
|
||||||
// ── View Switching ───────────────────
|
// ── View Switching ───────────────────
|
||||||
|
|
||||||
@@ -78,7 +77,7 @@ const NoteEditor = {
|
|||||||
|
|
||||||
async loadNotes(folder, searchQuery) {
|
async loadNotes(folder, searchQuery) {
|
||||||
if (!this.listEl) return;
|
if (!this.listEl) return;
|
||||||
this.listEl.innerHTML = '<div class="notes-loading">Loading…</div>';
|
this.listEl.innerHTML = '<div class="loading">Loading…</div>';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let notes;
|
let notes;
|
||||||
@@ -92,7 +91,7 @@ const NoteEditor = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (notes.length === 0) {
|
if (notes.length === 0) {
|
||||||
this.listEl.innerHTML = '<div class="notes-empty">' +
|
this.listEl.innerHTML = '<div class="empty-hint">' +
|
||||||
(searchQuery ? 'No results found' : 'No notes yet') + '</div>';
|
(searchQuery ? 'No results found' : 'No notes yet') + '</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -108,15 +107,15 @@ const NoteEditor = {
|
|||||||
const date = note.updated_at ? new Date(note.updated_at).toLocaleDateString() : '';
|
const date = note.updated_at ? new Date(note.updated_at).toLocaleDateString() : '';
|
||||||
item.innerHTML =
|
item.innerHTML =
|
||||||
'<div class="note-content-col">' +
|
'<div class="note-content-col">' +
|
||||||
'<div class="note-title">' + _neEsc(title) + '</div>' +
|
'<div class="note-title">' + esc(title) + '</div>' +
|
||||||
'<div class="note-preview">' + _neEsc(preview) + '</div>' +
|
'<div class="note-preview">' + esc(preview) + '</div>' +
|
||||||
'<div class="note-date">' + _neEsc(date) + '</div>' +
|
'<div class="note-date">' + esc(date) + '</div>' +
|
||||||
'</div>';
|
'</div>';
|
||||||
item.addEventListener('click', () => self.openNote(note.id));
|
item.addEventListener('click', () => self.openNote(note.id));
|
||||||
self.listEl.appendChild(item);
|
self.listEl.appendChild(item);
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.listEl.innerHTML = '<div class="notes-empty">Failed to load: ' + _neEsc(e.message) + '</div>';
|
this.listEl.innerHTML = '<div class="empty-hint">Failed to load: ' + esc(e.message) + '</div>';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -422,29 +421,15 @@ const NoteEditor = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── Lifecycle ───────────────────────
|
_cleanup() {
|
||||||
|
|
||||||
destroy() {
|
|
||||||
this._destroyCmEditor();
|
this._destroyCmEditor();
|
||||||
this._listeners.forEach(({ el, event, handler }) => el.removeEventListener(event, handler));
|
|
||||||
this._listeners = [];
|
|
||||||
this._editingNoteId = null;
|
this._editingNoteId = null;
|
||||||
this._currentNote = null;
|
this._currentNote = null;
|
||||||
NoteEditor._instances.delete(this.id);
|
|
||||||
},
|
},
|
||||||
|
}, NoteEditor);
|
||||||
|
|
||||||
_on(el, event, handler) {
|
NoteEditor._register(pfx, instance);
|
||||||
if (!el) return;
|
|
||||||
el.addEventListener(event, handler);
|
|
||||||
this._listeners.push({ el, event, handler });
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
NoteEditor._instances.set(pfx, instance);
|
|
||||||
return instance;
|
return instance;
|
||||||
},
|
},
|
||||||
|
|
||||||
get(id) { return this._instances.get(id) || null; },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function _neEsc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ async function openNotes() {
|
|||||||
|
|
||||||
async function loadNotesList(folder, searchQuery) {
|
async function loadNotesList(folder, searchQuery) {
|
||||||
const list = document.getElementById('notesList');
|
const list = document.getElementById('notesList');
|
||||||
list.innerHTML = '<div class="notes-loading">Loading…</div>';
|
list.innerHTML = '<div class="loading">Loading…</div>';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let data;
|
let data;
|
||||||
@@ -33,7 +33,7 @@ async function loadNotesList(folder, searchQuery) {
|
|||||||
data = await API.searchNotes(searchQuery);
|
data = await API.searchNotes(searchQuery);
|
||||||
const results = data.data || [];
|
const results = data.data || [];
|
||||||
if (results.length === 0) {
|
if (results.length === 0) {
|
||||||
list.innerHTML = '<div class="notes-empty">No results found</div>';
|
list.innerHTML = '<div class="empty-hint">No results found</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
list.innerHTML = results.map(n => _noteListItem(n, true)).join('');
|
list.innerHTML = results.map(n => _noteListItem(n, true)).join('');
|
||||||
@@ -42,13 +42,13 @@ async function loadNotesList(folder, searchQuery) {
|
|||||||
data = await API.listNotes(100, 0, folderVal, '', _notesSort);
|
data = await API.listNotes(100, 0, folderVal, '', _notesSort);
|
||||||
const notes = data.data || [];
|
const notes = data.data || [];
|
||||||
if (notes.length === 0) {
|
if (notes.length === 0) {
|
||||||
list.innerHTML = `<div class="notes-empty">${folderVal ? 'No notes in this folder' : 'No notes yet. Create one or ask the AI to save a note.'}</div>`;
|
list.innerHTML = `<div class="empty-hint">${folderVal ? 'No notes in this folder' : 'No notes yet. Create one or ask the AI to save a note.'}</div>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
list.innerHTML = notes.map(n => _noteListItem(n, false)).join('');
|
list.innerHTML = notes.map(n => _noteListItem(n, false)).join('');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
list.innerHTML = `<div class="notes-empty">Failed to load: ${esc(e.message)}</div>`;
|
list.innerHTML = `<div class="empty-hint">Failed to load: ${esc(e.message)}</div>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,7 +177,7 @@ function showNotesList() {
|
|||||||
_destroyNoteEditor();
|
_destroyNoteEditor();
|
||||||
// Reset list to loading state to prevent stale content flash
|
// Reset list to loading state to prevent stale content flash
|
||||||
const list = document.getElementById('notesList');
|
const list = document.getElementById('notesList');
|
||||||
if (list) list.innerHTML = '<div class="notes-loading">Loading…</div>';
|
if (list) list.innerHTML = '<div class="loading">Loading…</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
var _currentNote = null; // cached note for read mode
|
var _currentNote = null; // cached note for read mode
|
||||||
|
|||||||
@@ -126,8 +126,8 @@ const Notifications = {
|
|||||||
html += `<div class="${cls}" data-id="${n.id}" data-resource-type="${n.resource_type || ''}" data-resource-id="${n.resource_id || ''}" onclick="Notifications._onItemClick(this)">
|
html += `<div class="${cls}" data-id="${n.id}" data-resource-type="${n.resource_type || ''}" data-resource-id="${n.resource_id || ''}" onclick="Notifications._onItemClick(this)">
|
||||||
<span class="notif-dot">${dot}</span>
|
<span class="notif-dot">${dot}</span>
|
||||||
<div class="notif-body">
|
<div class="notif-body">
|
||||||
<div class="notif-title">${_esc(n.title)}</div>
|
<div class="notif-title">${esc(n.title)}</div>
|
||||||
${n.body ? `<div class="notif-detail">${_esc(n.body)}</div>` : ''}
|
${n.body ? `<div class="notif-detail">${esc(n.body)}</div>` : ''}
|
||||||
<div class="notif-time">${ago}</div>
|
<div class="notif-time">${ago}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
@@ -277,8 +277,8 @@ const Notifications = {
|
|||||||
<div class="notif-panel-row">
|
<div class="notif-panel-row">
|
||||||
<span class="notif-dot">${dot}</span>
|
<span class="notif-dot">${dot}</span>
|
||||||
<div class="notif-body">
|
<div class="notif-body">
|
||||||
<div class="notif-title">${_esc(n.title)}</div>
|
<div class="notif-title">${esc(n.title)}</div>
|
||||||
${n.body ? `<div class="notif-detail">${_esc(n.body)}</div>` : ''}
|
${n.body ? `<div class="notif-detail">${esc(n.body)}</div>` : ''}
|
||||||
<div class="notif-time">${ago}</div>
|
<div class="notif-time">${ago}</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="notif-delete" onclick="event.stopPropagation(); Notifications._deleteItem('${n.id}')" title="Delete">✕</button>
|
<button class="notif-delete" onclick="event.stopPropagation(); Notifications._deleteItem('${n.id}')" title="Delete">✕</button>
|
||||||
@@ -411,12 +411,6 @@ const Notifications = {
|
|||||||
|
|
||||||
// ── Helpers ──────────────────────────────────
|
// ── Helpers ──────────────────────────────────
|
||||||
|
|
||||||
function _esc(s) {
|
|
||||||
if (!s) return '';
|
|
||||||
const el = document.createElement('span');
|
|
||||||
el.textContent = s;
|
|
||||||
return el.innerHTML;
|
|
||||||
}
|
|
||||||
|
|
||||||
function _timeAgo(isoStr) {
|
function _timeAgo(isoStr) {
|
||||||
if (!isoStr) return '';
|
if (!isoStr) return '';
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ const Pages = {
|
|||||||
);
|
);
|
||||||
modelSelect.innerHTML = '<option value="">— select model —</option>' +
|
modelSelect.innerHTML = '<option value="">— select model —</option>' +
|
||||||
filtered.map(m =>
|
filtered.map(m =>
|
||||||
`<option value="${m.model_id}">${_esc(m.display_name || m.model_id)}</option>`
|
`<option value="${m.model_id}">${esc(m.display_name || m.model_id)}</option>`
|
||||||
).join('');
|
).join('');
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -302,11 +302,6 @@ function _val(id, setVal) {
|
|||||||
function _show(id) { const el = document.getElementById(id); if (el) el.style.display = ''; }
|
function _show(id) { const el = document.getElementById(id); if (el) el.style.display = ''; }
|
||||||
function _hide(id) { const el = document.getElementById(id); if (el) el.style.display = 'none'; }
|
function _hide(id) { const el = document.getElementById(id); if (el) el.style.display = 'none'; }
|
||||||
|
|
||||||
function _esc(s) {
|
|
||||||
const d = document.createElement('div');
|
|
||||||
d.textContent = s;
|
|
||||||
return d.innerHTML;
|
|
||||||
}
|
|
||||||
|
|
||||||
function _toast(msg, type) {
|
function _toast(msg, type) {
|
||||||
if (typeof UI !== 'undefined' && UI.toast) {
|
if (typeof UI !== 'undefined' && UI.toast) {
|
||||||
|
|||||||
@@ -271,7 +271,7 @@ const PanelRegistry = {
|
|||||||
const panel = this._panels[this._active];
|
const panel = this._panels[this._active];
|
||||||
slot.innerHTML = (panel.actions || [])
|
slot.innerHTML = (panel.actions || [])
|
||||||
.filter(a => !a.visible || a.visible())
|
.filter(a => !a.visible || a.visible())
|
||||||
.map(a => `<button class="side-panel-action-btn" id="${a.id || ''}" onclick="${a.onClickName || ''}" title="${_escPanel(a.title || '')}">${a.icon || ''}</button>`)
|
.map(a => `<button class="side-panel-action-btn" id="${a.id || ''}" onclick="${a.onClickName || ''}" title="${esc(a.title || '')}">${a.icon || ''}</button>`)
|
||||||
.join('');
|
.join('');
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -282,10 +282,6 @@ const PanelRegistry = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/** Minimal HTML escaping for panel labels/titles. */
|
/** Minimal HTML escaping for panel labels/titles. */
|
||||||
function _escPanel(s) {
|
|
||||||
if (!s) return '';
|
|
||||||
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Secondary Pane Fullscreen ──────────────
|
// ── Secondary Pane Fullscreen ──────────────
|
||||||
|
|
||||||
|
|||||||
@@ -68,8 +68,8 @@ function _showCreationDialog({ title, placeholder, withColor = false, defaultCol
|
|||||||
<input id="sbDialogColor" type="color" value="${defaultColor}" class="sb-dialog-color-input">
|
<input id="sbDialogColor" type="color" value="${defaultColor}" class="sb-dialog-color-input">
|
||||||
</div>` : ''}
|
</div>` : ''}
|
||||||
<div class="sb-dialog-actions">
|
<div class="sb-dialog-actions">
|
||||||
<button id="sbDialogCancel" class="sb-dialog-btn">Cancel</button>
|
<button id="sbDialogCancel" class="btn-small">Cancel</button>
|
||||||
<button id="sbDialogOK" class="sb-dialog-btn sb-dialog-btn-primary">${btnLabel}</button>
|
<button id="sbDialogOK" class="btn-small btn-primary">${btnLabel}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
@@ -142,7 +142,7 @@ async function renameProject(projectId) {
|
|||||||
async function deleteProject(projectId) {
|
async function deleteProject(projectId) {
|
||||||
const proj = App.projects.find(p => p.id === projectId);
|
const proj = App.projects.find(p => p.id === projectId);
|
||||||
if (!proj) return;
|
if (!proj) return;
|
||||||
if (!confirm(`Delete project "${proj.name}"?\nChats inside will be kept but unassigned.`)) return;
|
if (!await showConfirm(`Delete project "${proj.name}"?\nChats inside will be kept but unassigned.`)) return;
|
||||||
try {
|
try {
|
||||||
await API.deleteProject(projectId);
|
await API.deleteProject(projectId);
|
||||||
App.projects = App.projects.filter(p => p.id !== projectId);
|
App.projects = App.projects.filter(p => p.id !== projectId);
|
||||||
|
|||||||
@@ -465,13 +465,17 @@ function _renderCmdResults(query) {
|
|||||||
// ── Settings Listeners (extracted from initListeners) ──
|
// ── Settings Listeners (extracted from initListeners) ──
|
||||||
|
|
||||||
function _initSettingsListeners() {
|
function _initSettingsListeners() {
|
||||||
// Settings modal
|
// Settings modal elements only exist on the settings surface.
|
||||||
document.getElementById('settingsCloseBtn').addEventListener('click', UI.closeSettings);
|
// On chat surface, openSettings() navigates to /settings/ instead.
|
||||||
document.getElementById('settingsSaveBtn').addEventListener('click', handleSaveSettings);
|
const settingsModel = document.getElementById('settingsModel');
|
||||||
|
if (!settingsModel) return;
|
||||||
|
|
||||||
|
document.getElementById('settingsCloseBtn')?.addEventListener('click', UI.closeSettings);
|
||||||
|
document.getElementById('settingsSaveBtn')?.addEventListener('click', handleSaveSettings);
|
||||||
document.querySelectorAll('.settings-tab').forEach(tab => {
|
document.querySelectorAll('.settings-tab').forEach(tab => {
|
||||||
tab.addEventListener('click', () => UI.switchSettingsTab(tab.dataset.stab));
|
tab.addEventListener('click', () => UI.switchSettingsTab(tab.dataset.stab));
|
||||||
});
|
});
|
||||||
document.getElementById('settingsModel').addEventListener('change', function() {
|
settingsModel.addEventListener('change', function() {
|
||||||
const model = App.findModel(this.value);
|
const model = App.findModel(this.value);
|
||||||
const caps = model?.capabilities || {};
|
const caps = model?.capabilities || {};
|
||||||
const hint = document.getElementById('settingsMaxHint');
|
const hint = document.getElementById('settingsMaxHint');
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
/**
|
|
||||||
* surface-nav.js — Navigation overrides for server-rendered surfaces.
|
|
||||||
*
|
|
||||||
* Replaces SPA modal-based openSettings/openAdmin with page navigation
|
|
||||||
* to the dedicated /settings and /admin surface routes.
|
|
||||||
*
|
|
||||||
* Load AFTER ui-core.js and ui-admin.js so it overrides their methods.
|
|
||||||
*
|
|
||||||
* v0.22.8 — FE refactor: surfaces replace modals
|
|
||||||
*/
|
|
||||||
(function() {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const base = window.__BASE__ || '';
|
|
||||||
|
|
||||||
// ── Settings: navigate to /settings surface ──────────
|
|
||||||
if (typeof UI !== 'undefined') {
|
|
||||||
UI.openSettings = function(section) {
|
|
||||||
const sec = section || 'general';
|
|
||||||
window.location.href = base + '/settings/' + sec;
|
|
||||||
};
|
|
||||||
UI.closeSettings = function() {
|
|
||||||
// Navigate back to chat if called from settings surface
|
|
||||||
window.location.href = base + '/';
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Admin: navigate to /admin surface ────────────────
|
|
||||||
if (typeof UI !== 'undefined') {
|
|
||||||
UI.openAdmin = function(cat, section) {
|
|
||||||
const sec = section || 'users';
|
|
||||||
window.location.href = base + '/admin/' + sec;
|
|
||||||
};
|
|
||||||
UI.closeAdmin = function() {
|
|
||||||
window.location.href = base + '/';
|
|
||||||
};
|
|
||||||
UI.openAdminSection = function(section) {
|
|
||||||
window.location.href = base + '/admin/' + (section || 'users');
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('[surface-nav] Navigation overrides active — settings/admin use server surfaces');
|
|
||||||
})();
|
|
||||||
@@ -66,26 +66,19 @@ Object.assign(UI, {
|
|||||||
|
|
||||||
// ── Open / Close ──────────────────────────
|
// ── Open / Close ──────────────────────────
|
||||||
openAdmin(cat, section) {
|
openAdmin(cat, section) {
|
||||||
UI._adminCat = cat || 'people';
|
const base = window.__BASE__ || '';
|
||||||
UI._adminSection = section || ADMIN_SECTIONS[UI._adminCat][0];
|
window.location.href = base + '/admin/' + (section || 'users');
|
||||||
const panel = document.getElementById('adminPanel');
|
|
||||||
panel.style.display = 'flex';
|
|
||||||
// Show current user email
|
|
||||||
const userEl = document.getElementById('adminCurrentUser');
|
|
||||||
if (userEl && API.user) userEl.textContent = API.user.email || API.user.username || '';
|
|
||||||
UI._renderAdminNav();
|
|
||||||
UI._switchAdminSection(UI._adminSection);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
closeAdmin() {
|
closeAdmin() {
|
||||||
document.getElementById('adminPanel').style.display = 'none';
|
const base = window.__BASE__ || '';
|
||||||
|
window.location.href = base + '/';
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── Navigate to specific section ──────────
|
// ── Navigate to specific section ──────────
|
||||||
// Called from command palette, deep links, etc.
|
|
||||||
openAdminSection(section) {
|
openAdminSection(section) {
|
||||||
const cat = _adminCatForSection(section);
|
const base = window.__BASE__ || '';
|
||||||
UI.openAdmin(cat, section);
|
window.location.href = base + '/admin/' + (section || 'users');
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── Category switch ───────────────────────
|
// ── Category switch ───────────────────────
|
||||||
@@ -128,21 +121,6 @@ Object.assign(UI, {
|
|||||||
await ADMIN_LOADERS[section]?.();
|
await ADMIN_LOADERS[section]?.();
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── Backward compat: switchAdminTab still works ──
|
|
||||||
// (called by some inline onclick handlers and admin-handlers.js)
|
|
||||||
async switchAdminTab(tab) {
|
|
||||||
const cat = _adminCatForSection(tab);
|
|
||||||
if (document.getElementById('adminPanel').style.display !== 'flex') {
|
|
||||||
UI.openAdmin(cat, tab);
|
|
||||||
} else {
|
|
||||||
if (cat !== UI._adminCat) {
|
|
||||||
UI._adminCat = cat;
|
|
||||||
UI._renderAdminNav();
|
|
||||||
}
|
|
||||||
await UI._switchAdminSection(tab);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
openTeamAdmin() {
|
openTeamAdmin() {
|
||||||
const adminTeams = (UI._myTeams || []).filter(t => t.my_role === 'admin');
|
const adminTeams = (UI._myTeams || []).filter(t => t.my_role === 'admin');
|
||||||
if (adminTeams.length === 0) {
|
if (adminTeams.length === 0) {
|
||||||
@@ -233,12 +211,12 @@ Object.assign(UI, {
|
|||||||
<td class="admin-user-time">${lastSeen}</td>
|
<td class="admin-user-time">${lastSeen}</td>
|
||||||
<td class="admin-actions-cell">
|
<td class="admin-actions-cell">
|
||||||
${isPending
|
${isPending
|
||||||
? `<button class="btn-icon" onclick="showApproveForm('${u.id}','${esc(u.username)}')" title="Approve">✓</button>`
|
? `<button class="icon-btn" onclick="showApproveForm('${u.id}','${esc(u.username)}')" title="Approve">✓</button>`
|
||||||
: `<button class="btn-icon" onclick="toggleUserActive('${u.id}',false)" title="Disable">⏸</button>`
|
: `<button class="icon-btn" onclick="toggleUserActive('${u.id}',false)" title="Disable">⏸</button>`
|
||||||
}
|
}
|
||||||
<button class="btn-icon" onclick="toggleUserRole('${u.id}','${u.role === 'admin' ? 'user' : 'admin'}')" title="${u.role === 'admin' ? 'Demote' : 'Promote'}">⇅</button>
|
<button class="icon-btn" onclick="toggleUserRole('${u.id}','${u.role === 'admin' ? 'user' : 'admin'}')" title="${u.role === 'admin' ? 'Demote' : 'Promote'}">⇅</button>
|
||||||
<button class="btn-icon" onclick="adminResetUserPassword('${u.id}','${esc(u.username)}')" title="Reset PW">🔑</button>
|
<button class="icon-btn" onclick="adminResetUserPassword('${u.id}','${esc(u.username)}')" title="Reset PW">🔑</button>
|
||||||
<button class="btn-icon btn-icon-danger" onclick="deleteUser('${u.id}','${esc(u.username)}')" title="Delete">🗑</button>
|
<button class="icon-btn icon-btn-danger" onclick="deleteUser('${u.id}','${esc(u.username)}')" title="Delete">🗑</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
@@ -439,9 +417,9 @@ Object.assign(UI, {
|
|||||||
<td><span class="badge-provider-type">${esc(ext.tier)}</span></td>
|
<td><span class="badge-provider-type">${esc(ext.tier)}</span></td>
|
||||||
<td style="font-size:12px;color:var(--text-3);font-family:var(--font-mono,monospace)">v${esc(ext.version)}</td>
|
<td style="font-size:12px;color:var(--text-3);font-family:var(--font-mono,monospace)">v${esc(ext.version)}</td>
|
||||||
<td class="admin-actions-cell">
|
<td class="admin-actions-cell">
|
||||||
<button class="btn-icon" onclick="editAdminExtension('${ext.id}')" title="Edit">✎</button>
|
<button class="icon-btn" onclick="editAdminExtension('${ext.id}')" title="Edit">✎</button>
|
||||||
<button class="btn-icon" onclick="toggleAdminExtension('${ext.id}',${!ext.is_enabled})" title="${ext.is_enabled ? 'Disable' : 'Enable'}">${ext.is_enabled ? '⏸' : '▶'}</button>
|
<button class="icon-btn" onclick="toggleAdminExtension('${ext.id}',${!ext.is_enabled})" title="${ext.is_enabled ? 'Disable' : 'Enable'}">${ext.is_enabled ? '⏸' : '▶'}</button>
|
||||||
<button class="btn-icon btn-icon-danger" onclick="deleteAdminExtension('${ext.id}','${esc(ext.name)}')" title="Uninstall">🗑</button>
|
<button class="icon-btn icon-btn-danger" onclick="deleteAdminExtension('${ext.id}','${esc(ext.name)}')" title="Uninstall">🗑</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
}).join('')}</tbody>
|
}).join('')}</tbody>
|
||||||
@@ -746,10 +724,10 @@ Object.assign(UI, {
|
|||||||
<td style="font-size:12px;color:var(--text-3);font-family:var(--font-mono,monospace)">${esc(p.base_model_id)}</td>
|
<td style="font-size:12px;color:var(--text-3);font-family:var(--font-mono,monospace)">${esc(p.base_model_id)}</td>
|
||||||
<td>${p.is_active ? '<span class="badge-active">active</span>' : '<span class="badge-disabled">inactive</span>'}</td>
|
<td>${p.is_active ? '<span class="badge-active">active</span>' : '<span class="badge-disabled">inactive</span>'}</td>
|
||||||
<td class="admin-actions-cell">
|
<td class="admin-actions-cell">
|
||||||
<button class="btn-icon" onclick="UI.openGrantPicker('persona','${p.id}','${esc(p.name)}','[data-grant-persona="${p.id}"]')" title="Access">🔒</button>
|
<button class="icon-btn" onclick="UI.openGrantPicker('persona','${p.id}','${esc(p.name)}','[data-grant-persona="${p.id}"]')" title="Access">🔒</button>
|
||||||
<button class="btn-icon" onclick="editAdminPersona('${p.id}')" title="Edit">✎</button>
|
<button class="icon-btn" onclick="editAdminPersona('${p.id}')" title="Edit">✎</button>
|
||||||
<button class="btn-icon" onclick="toggleAdminPersona('${p.id}',${!p.is_active})" title="${p.is_active ? 'Disable' : 'Enable'}">${p.is_active ? '⏸' : '▶'}</button>
|
<button class="icon-btn" onclick="toggleAdminPersona('${p.id}',${!p.is_active})" title="${p.is_active ? 'Disable' : 'Enable'}">${p.is_active ? '⏸' : '▶'}</button>
|
||||||
<button class="btn-icon btn-icon-danger" onclick="deleteAdminPersona('${p.id}','${esc(p.name)}')" title="Delete">🗑</button>
|
<button class="icon-btn icon-btn-danger" onclick="deleteAdminPersona('${p.id}','${esc(p.name)}')" title="Delete">🗑</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
}).join('')}</tbody>
|
}).join('')}</tbody>
|
||||||
@@ -788,9 +766,9 @@ Object.assign(UI, {
|
|||||||
</td>
|
</td>
|
||||||
<td style="font-size:12px;color:var(--text-2)">${t.member_count} member${t.member_count !== 1 ? 's' : ''}</td>
|
<td style="font-size:12px;color:var(--text-2)">${t.member_count} member${t.member_count !== 1 ? 's' : ''}</td>
|
||||||
<td class="admin-actions-cell">
|
<td class="admin-actions-cell">
|
||||||
<button class="btn-icon" onclick="UI.openTeamDetail('${t.id}','${esc(t.name)}')" title="Members">👥</button>
|
<button class="icon-btn" onclick="UI.openTeamDetail('${t.id}','${esc(t.name)}')" title="Members">👥</button>
|
||||||
<button class="btn-icon" onclick="toggleTeamActive('${t.id}',${!t.is_active})" title="${t.is_active ? 'Disable' : 'Enable'}">${t.is_active ? '⏸' : '▶'}</button>
|
<button class="icon-btn" onclick="toggleTeamActive('${t.id}',${!t.is_active})" title="${t.is_active ? 'Disable' : 'Enable'}">${t.is_active ? '⏸' : '▶'}</button>
|
||||||
<button class="btn-icon btn-icon-danger" onclick="deleteTeam('${t.id}','${esc(t.name)}')" title="Delete">🗑</button>
|
<button class="icon-btn icon-btn-danger" onclick="deleteTeam('${t.id}','${esc(t.name)}')" title="Delete">🗑</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>`).join('')}</tbody>
|
</tr>`).join('')}</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -843,7 +821,7 @@ Object.assign(UI, {
|
|||||||
${m.user_role === 'admin' ? '<span class="badge-admin" style="margin-left:4px;font-size:9px">sys-admin</span>' : ''}
|
${m.user_role === 'admin' ? '<span class="badge-admin" style="margin-left:4px;font-size:9px">sys-admin</span>' : ''}
|
||||||
</td>
|
</td>
|
||||||
<td class="admin-actions-cell">
|
<td class="admin-actions-cell">
|
||||||
<button class="btn-icon btn-icon-danger" onclick="removeTeamMember('${teamId}','${m.id}','${esc(m.email)}')" title="Remove">🗑</button>
|
<button class="icon-btn icon-btn-danger" onclick="removeTeamMember('${teamId}','${m.id}','${esc(m.email)}')" title="Remove">🗑</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>`).join('')}</tbody>
|
</tr>`).join('')}</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -898,8 +876,8 @@ Object.assign(UI, {
|
|||||||
<td>${scopeBadge}</td>
|
<td>${scopeBadge}</td>
|
||||||
<td style="font-size:12px;color:var(--text-2)">${g.member_count} member${g.member_count !== 1 ? 's' : ''}</td>
|
<td style="font-size:12px;color:var(--text-2)">${g.member_count} member${g.member_count !== 1 ? 's' : ''}</td>
|
||||||
<td class="admin-actions-cell">
|
<td class="admin-actions-cell">
|
||||||
<button class="btn-icon" onclick="UI.openGroupDetail('${g.id}','${esc(g.name)}')" title="Members">👥</button>
|
<button class="icon-btn" onclick="UI.openGroupDetail('${g.id}','${esc(g.name)}')" title="Members">👥</button>
|
||||||
<button class="btn-icon btn-icon-danger" onclick="deleteGroup('${g.id}','${esc(g.name)}')" title="Delete">🗑</button>
|
<button class="icon-btn icon-btn-danger" onclick="deleteGroup('${g.id}','${esc(g.name)}')" title="Delete">🗑</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
}).join('')}</tbody>
|
}).join('')}</tbody>
|
||||||
@@ -1022,7 +1000,7 @@ Object.assign(UI, {
|
|||||||
</td>
|
</td>
|
||||||
<td class="admin-user-time">${new Date(m.added_at).toLocaleDateString()}</td>
|
<td class="admin-user-time">${new Date(m.added_at).toLocaleDateString()}</td>
|
||||||
<td class="admin-actions-cell">
|
<td class="admin-actions-cell">
|
||||||
<button class="btn-icon btn-icon-danger" onclick="removeGroupMember('${groupId}','${m.user_id}','${esc(m.username || m.email)}')" title="Remove">🗑</button>
|
<button class="icon-btn icon-btn-danger" onclick="removeGroupMember('${groupId}','${m.user_id}','${esc(m.username || m.email)}')" title="Remove">🗑</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>`).join('')}</tbody>
|
</tr>`).join('')}</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -1196,9 +1174,9 @@ Object.assign(UI, {
|
|||||||
<td class="admin-user-time">${p.priority}</td>
|
<td class="admin-user-time">${p.priority}</td>
|
||||||
<td>${p.is_active ? '<span class="badge-active">active</span>' : '<span class="badge-disabled">inactive</span>'}</td>
|
<td>${p.is_active ? '<span class="badge-active">active</span>' : '<span class="badge-disabled">inactive</span>'}</td>
|
||||||
<td class="admin-actions-cell">
|
<td class="admin-actions-cell">
|
||||||
<button class="btn-icon" onclick="UI._showRoutingForm('${esc(p.id)}')" title="Edit">✎</button>
|
<button class="icon-btn" onclick="UI._showRoutingForm('${esc(p.id)}')" title="Edit">✎</button>
|
||||||
<button class="btn-icon" onclick="UI._toggleRoutingPolicy('${esc(p.id)}',${!p.is_active})" title="${p.is_active ? 'Disable' : 'Enable'}">${p.is_active ? '⏸' : '▶'}</button>
|
<button class="icon-btn" onclick="UI._toggleRoutingPolicy('${esc(p.id)}',${!p.is_active})" title="${p.is_active ? 'Disable' : 'Enable'}">${p.is_active ? '⏸' : '▶'}</button>
|
||||||
<button class="btn-icon btn-icon-danger" onclick="UI._deleteRoutingPolicy('${esc(p.id)}','${esc(p.name)}')" title="Delete">🗑</button>
|
<button class="icon-btn icon-btn-danger" onclick="UI._deleteRoutingPolicy('${esc(p.id)}','${esc(p.name)}')" title="Delete">🗑</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>`).join('')}</tbody>
|
</tr>`).join('')}</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -1377,34 +1377,14 @@ const UI = {
|
|||||||
|
|
||||||
// ── Settings Modal ───────────────────────
|
// ── Settings Modal ───────────────────────
|
||||||
|
|
||||||
openSettings() {
|
openSettings(section) {
|
||||||
document.getElementById('settingsModel').value = App.settings.model;
|
const base = window.__BASE__ || '';
|
||||||
document.getElementById('settingsSystemPrompt').value = App.settings.systemPrompt;
|
window.location.href = base + '/settings/' + (section || 'general');
|
||||||
document.getElementById('settingsMaxTokens').value = App.settings.maxTokens || '';
|
},
|
||||||
document.getElementById('settingsTemp').value = App.settings.temperature;
|
closeSettings() {
|
||||||
document.getElementById('settingsThinking').checked = App.settings.showThinking;
|
const base = window.__BASE__ || '';
|
||||||
document.getElementById('profileChangePwForm').style.display = 'none';
|
window.location.href = base + '/';
|
||||||
|
|
||||||
// Show model's max output in the hint
|
|
||||||
const caps = this.getSelectedModelCaps();
|
|
||||||
const hint = document.getElementById('settingsMaxHint');
|
|
||||||
if (hint) {
|
|
||||||
if (caps.max_output_tokens > 0) {
|
|
||||||
const k = (caps.max_output_tokens / 1000).toFixed(0) + 'K';
|
|
||||||
hint.textContent = `(model max: ${k})`;
|
|
||||||
} else {
|
|
||||||
hint.textContent = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
UI.loadProfileIntoSettings();
|
|
||||||
UI.loadMyTeams();
|
|
||||||
UI.checkUserProvidersAllowed();
|
|
||||||
UI.checkUserPersonasAllowed();
|
|
||||||
UI.switchSettingsTab('general');
|
|
||||||
openModal('settingsModal');
|
|
||||||
},
|
},
|
||||||
closeSettings() { closeModal('settingsModal'); },
|
|
||||||
|
|
||||||
switchSettingsTab(tab) {
|
switchSettingsTab(tab) {
|
||||||
document.querySelectorAll('.settings-tab').forEach(t => t.classList.toggle('active', t.dataset.stab === tab));
|
document.querySelectorAll('.settings-tab').forEach(t => t.classList.toggle('active', t.dataset.stab === tab));
|
||||||
|
|||||||
@@ -1,17 +1,8 @@
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
// Chat Switchboard – UI Formatting & Helpers
|
// Chat Switchboard – UI Formatting & Helpers
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// Pure utility layer: esc(), markdown rendering, code blocks,
|
// Markdown rendering, code blocks, preview panel, time formatting.
|
||||||
// preview panel, time formatting. Loaded after panels.js.
|
// esc() is in ui-primitives.js (loaded globally in base.html).
|
||||||
|
|
||||||
// ── HTML Escaping ───────────────────────────
|
|
||||||
|
|
||||||
function esc(s) {
|
|
||||||
if (!s) return '';
|
|
||||||
const d = document.createElement('div');
|
|
||||||
d.textContent = s;
|
|
||||||
return d.innerHTML;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Message Formatting ──────────────────────
|
// ── Message Formatting ──────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -1,199 +0,0 @@
|
|||||||
// ==========================================
|
|
||||||
// Chat Switchboard - UI Primitives Additions
|
|
||||||
// ==========================================
|
|
||||||
// v0.22.7: Toast, Badge, IconBtn, Theme toggle, confirm dialog, avatar upload.
|
|
||||||
// Load AFTER ui-primitives.js in base.html.
|
|
||||||
|
|
||||||
// -- Toast ------------------------------------------------
|
|
||||||
var Toast = {
|
|
||||||
_container: null,
|
|
||||||
_init: function() {
|
|
||||||
if (this._container) return;
|
|
||||||
this._container = document.getElementById('toastContainer');
|
|
||||||
if (!this._container) {
|
|
||||||
this._container = document.createElement('div');
|
|
||||||
this._container.id = 'toastContainer';
|
|
||||||
this._container.className = 'toast-stack';
|
|
||||||
document.body.appendChild(this._container);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
show: function(msg, type) {
|
|
||||||
type = type || 'info';
|
|
||||||
this._init();
|
|
||||||
var el = document.createElement('div');
|
|
||||||
el.className = 'toast toast-' + type;
|
|
||||||
el.innerHTML = '<span class="toast-icon"></span><span class="toast-msg">' + (typeof esc === 'function' ? esc(msg) : msg) + '</span>';
|
|
||||||
this._container.appendChild(el);
|
|
||||||
setTimeout(function() {
|
|
||||||
el.classList.add('toast-exit');
|
|
||||||
setTimeout(function() { el.remove(); }, 300);
|
|
||||||
}, 3000);
|
|
||||||
},
|
|
||||||
success: function(msg) { this.show(msg, 'success'); },
|
|
||||||
error: function(msg) { this.show(msg, 'error'); },
|
|
||||||
warning: function(msg) { this.show(msg, 'warning'); },
|
|
||||||
info: function(msg) { this.show(msg, 'info'); },
|
|
||||||
};
|
|
||||||
|
|
||||||
// -- Badge ------------------------------------------------
|
|
||||||
function renderBadge(text, color) {
|
|
||||||
color = color || 'muted';
|
|
||||||
var e = typeof esc === 'function' ? esc : function(s) { return s; };
|
|
||||||
return '<span class="badge badge-' + e(color) + '">' + e(text) + '</span>';
|
|
||||||
}
|
|
||||||
|
|
||||||
// -- Icon SVG ---------------------------------------------
|
|
||||||
var _ICONS = {
|
|
||||||
x: '<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>',
|
|
||||||
back: '<line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/>',
|
|
||||||
plus: '<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>',
|
|
||||||
edit: '<path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.12 2.12 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/>',
|
|
||||||
trash: '<polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/>',
|
|
||||||
search: '<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>',
|
|
||||||
send: '<line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/>',
|
|
||||||
user: '<path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"/><circle cx="12" cy="7" r="4"/>',
|
|
||||||
users: '<path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 00-3-3.87"/><path d="M16 3.13a4 4 0 010 7.75"/>',
|
|
||||||
key: '<path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 11-7.778 7.778 5.5 5.5 0 010-7.777L12 4l3.5 3.5L18 5l3-3"/>',
|
|
||||||
settings:'<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9c.2.65.7 1.15 1.33 1.34H21a2 2 0 010 4h-.09c-.63.2-1.13.7-1.33 1.34z"/>',
|
|
||||||
cpu: '<rect x="4" y="4" width="16" height="16" rx="2"/><rect x="9" y="9" width="6" height="6"/><line x1="9" y1="1" x2="9" y2="4"/><line x1="15" y1="1" x2="15" y2="4"/><line x1="9" y1="20" x2="9" y2="23"/><line x1="15" y1="20" x2="15" y2="23"/><line x1="20" y1="9" x2="23" y2="9"/><line x1="20" y1="14" x2="23" y2="14"/><line x1="1" y1="9" x2="4" y2="9"/><line x1="1" y1="14" x2="4" y2="14"/>',
|
|
||||||
eye: '<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/>',
|
|
||||||
chart: '<line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/>',
|
|
||||||
shield: '<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>',
|
|
||||||
msg: '<path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/>',
|
|
||||||
globe: '<circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z"/>',
|
|
||||||
};
|
|
||||||
|
|
||||||
function renderIcon(name, size) {
|
|
||||||
size = size || 16;
|
|
||||||
var d = _ICONS[name];
|
|
||||||
if (!d) return '';
|
|
||||||
return '<svg width="' + size + '" height="' + size + '" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' + d + '</svg>';
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderIconBtn(icon, opts) {
|
|
||||||
opts = opts || {};
|
|
||||||
var title = opts.title || '';
|
|
||||||
var onclick = opts.onclick || '';
|
|
||||||
var cls = opts.cls || '';
|
|
||||||
var size = opts.size || 16;
|
|
||||||
var activeClass = opts.active ? ' active' : '';
|
|
||||||
var e = typeof esc === 'function' ? esc : function(s) { return s; };
|
|
||||||
return '<button class="icon-btn' + activeClass + ' ' + cls + '" title="' + e(title) + '"' +
|
|
||||||
(onclick ? ' onclick="' + onclick + '"' : '') + '>' + renderIcon(icon, size) + '</button>';
|
|
||||||
}
|
|
||||||
|
|
||||||
// -- Theme ------------------------------------------------
|
|
||||||
var Theme = {
|
|
||||||
_key: 'switchboard_theme',
|
|
||||||
init: function() {
|
|
||||||
var saved = localStorage.getItem(this._key) || 'system';
|
|
||||||
this.set(saved);
|
|
||||||
},
|
|
||||||
set: function(mode) {
|
|
||||||
localStorage.setItem(this._key, mode);
|
|
||||||
if (mode === 'system') document.documentElement.removeAttribute('data-theme');
|
|
||||||
else document.documentElement.setAttribute('data-theme', mode);
|
|
||||||
},
|
|
||||||
get: function() { return localStorage.getItem(this._key) || 'system'; },
|
|
||||||
resolved: function() {
|
|
||||||
var mode = this.get();
|
|
||||||
if (mode !== 'system') return mode;
|
|
||||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
||||||
},
|
|
||||||
renderToggle: function(containerId) {
|
|
||||||
var el = document.getElementById(containerId);
|
|
||||||
if (!el) return;
|
|
||||||
var current = this.get();
|
|
||||||
var options = [
|
|
||||||
{ id: 'light', label: 'Light', icon: '\u2600' },
|
|
||||||
{ id: 'dark', label: 'Dark', icon: '\u263E' },
|
|
||||||
{ id: 'system',label: 'System',icon: '\u229E' },
|
|
||||||
];
|
|
||||||
var self = this;
|
|
||||||
el.innerHTML = '<div class="theme-toggle">' + options.map(function(o) {
|
|
||||||
return '<button class="theme-toggle__btn' + (current === o.id ? ' active' : '') + '" data-theme="' + o.id + '" title="' + o.label + '">' + o.icon + '</button>';
|
|
||||||
}).join('') + '</div>';
|
|
||||||
el.querySelectorAll('.theme-toggle__btn').forEach(function(btn) {
|
|
||||||
btn.addEventListener('click', function() {
|
|
||||||
self.set(btn.dataset.theme);
|
|
||||||
self.renderToggle(containerId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// -- Confirm Dialog (enhanced) ----------------------------
|
|
||||||
function showConfirmDialog(opts) {
|
|
||||||
opts = opts || {};
|
|
||||||
var title = opts.title || 'Confirm';
|
|
||||||
var message = opts.message || 'Are you sure?';
|
|
||||||
var confirmLabel = opts.confirmLabel || 'Confirm';
|
|
||||||
var cancelLabel = opts.cancelLabel || 'Cancel';
|
|
||||||
var variant = opts.variant || 'primary';
|
|
||||||
var onConfirm = opts.onConfirm;
|
|
||||||
var onCancel = opts.onCancel;
|
|
||||||
var e = typeof esc === 'function' ? esc : function(s) { return s; };
|
|
||||||
|
|
||||||
var overlay = document.getElementById('confirmOverlay');
|
|
||||||
if (!overlay) {
|
|
||||||
overlay = document.createElement('div');
|
|
||||||
overlay.id = 'confirmOverlay';
|
|
||||||
overlay.className = 'confirm-overlay';
|
|
||||||
overlay.style.display = 'none';
|
|
||||||
document.body.appendChild(overlay);
|
|
||||||
}
|
|
||||||
|
|
||||||
overlay.innerHTML =
|
|
||||||
'<div class="confirm-dialog">' +
|
|
||||||
'<div class="confirm-dialog__title">' + e(title) + '</div>' +
|
|
||||||
'<div class="confirm-dialog__msg">' + e(message) + '</div>' +
|
|
||||||
'<div class="confirm-dialog__actions">' +
|
|
||||||
'<button class="btn btn-ghost" id="confirmCancelBtn">' + e(cancelLabel) + '</button>' +
|
|
||||||
'<button class="btn btn-' + variant + '" id="confirmOkBtn">' + e(confirmLabel) + '</button>' +
|
|
||||||
'</div>' +
|
|
||||||
'</div>';
|
|
||||||
overlay.style.display = 'flex';
|
|
||||||
|
|
||||||
function close() { overlay.style.display = 'none'; overlay.innerHTML = ''; }
|
|
||||||
document.getElementById('confirmCancelBtn').addEventListener('click', function() { close(); if (onCancel) onCancel(); });
|
|
||||||
document.getElementById('confirmOkBtn').addEventListener('click', function() { close(); if (onConfirm) onConfirm(); });
|
|
||||||
overlay.addEventListener('click', function(ev) { if (ev.target === overlay) { close(); if (onCancel) onCancel(); } });
|
|
||||||
}
|
|
||||||
|
|
||||||
// -- Avatar Upload Component ------------------------------
|
|
||||||
function mountAvatarUpload(containerEl, opts) {
|
|
||||||
opts = opts || {};
|
|
||||||
var current = opts.current;
|
|
||||||
var size = opts.size || 80;
|
|
||||||
var onUpload = opts.onUpload;
|
|
||||||
var input = document.createElement('input');
|
|
||||||
input.type = 'file';
|
|
||||||
input.accept = 'image/*';
|
|
||||||
input.style.display = 'none';
|
|
||||||
|
|
||||||
containerEl.className = 'avatar-upload';
|
|
||||||
containerEl.style.width = size + 'px';
|
|
||||||
containerEl.style.height = size + 'px';
|
|
||||||
|
|
||||||
function render(src) {
|
|
||||||
var e = typeof esc === 'function' ? esc : function(s) { return s; };
|
|
||||||
if (src) containerEl.innerHTML = '<img src="' + e(src) + '" alt="Avatar">';
|
|
||||||
else containerEl.innerHTML = '<div class="avatar-upload__placeholder">Upload<br>Photo</div>';
|
|
||||||
containerEl.appendChild(input);
|
|
||||||
}
|
|
||||||
|
|
||||||
render(current || null);
|
|
||||||
containerEl.addEventListener('click', function() { input.click(); });
|
|
||||||
input.addEventListener('change', function() {
|
|
||||||
var file = input.files[0];
|
|
||||||
if (!file) return;
|
|
||||||
var reader = new FileReader();
|
|
||||||
reader.onload = function(ev) {
|
|
||||||
render(ev.target.result);
|
|
||||||
if (onUpload) onUpload(file, ev.target.result);
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
});
|
|
||||||
|
|
||||||
return { setImage: render };
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
// ── Chat Switchboard — UI Primitives ────────────────────────────────────────
|
// ── Chat Switchboard — UI Primitives ────────────────────────────────────────
|
||||||
// Single source of truth for constants + reusable rendering functions.
|
// Single source of truth for constants + reusable rendering functions.
|
||||||
// Loaded after ui-format.js, before ui-core.js.
|
// Loaded in base.html before all components and surfaces.
|
||||||
// Follows the renderPersonaForm() pattern: (container, options) → control object.
|
// Follows the renderPersonaForm() pattern: (container, options) → control object.
|
||||||
// Designed for extension hooks: registries are open for .add(), primitives
|
// Designed for extension hooks: registries are open for .add(), primitives
|
||||||
// accept options for customization, and return handles for programmatic control.
|
// accept options for customization, and return handles for programmatic control.
|
||||||
@@ -8,6 +8,67 @@
|
|||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
// §0 CORE UTILITIES
|
||||||
|
// ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/** HTML-escape a string for safe insertion into innerHTML and attributes. */
|
||||||
|
function esc(s) {
|
||||||
|
if (!s) return '';
|
||||||
|
const d = document.createElement('div');
|
||||||
|
d.textContent = s;
|
||||||
|
return d.innerHTML.replace(/"/g, '"');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
// §0b COMPONENT LIFECYCLE
|
||||||
|
// ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a component registry (factory namespace with instance tracking).
|
||||||
|
*
|
||||||
|
* @param {string} name - Component name (for debug)
|
||||||
|
* @returns {{ primary, _instances, _register, _unregister, get }}
|
||||||
|
*/
|
||||||
|
function createComponentRegistry(name) {
|
||||||
|
return {
|
||||||
|
primary: null,
|
||||||
|
_instances: new Map(),
|
||||||
|
_register(id, instance) { this._instances.set(id, instance); },
|
||||||
|
_unregister(id) {
|
||||||
|
this._instances.delete(id);
|
||||||
|
if (this.primary && this.primary.id === id) this.primary = null;
|
||||||
|
},
|
||||||
|
get(id) { return this._instances.get(id) || null; },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mix lifecycle methods into a component instance.
|
||||||
|
* Adds _listeners array, _on() for tracked event binding, and destroy()
|
||||||
|
* for automatic cleanup + registry removal.
|
||||||
|
* Components needing extra teardown define _cleanup() which is called first.
|
||||||
|
*
|
||||||
|
* @param {Object} instance - The instance object (must have .id)
|
||||||
|
* @param {Object} registry - The component registry (from createComponentRegistry)
|
||||||
|
* @returns {Object} The instance, mutated with lifecycle methods
|
||||||
|
*/
|
||||||
|
function componentMixin(instance, registry) {
|
||||||
|
instance._listeners = [];
|
||||||
|
instance._on = function(el, event, handler) {
|
||||||
|
if (!el) return;
|
||||||
|
el.addEventListener(event, handler);
|
||||||
|
this._listeners.push({ el, event, handler });
|
||||||
|
};
|
||||||
|
instance.destroy = function() {
|
||||||
|
if (typeof this._cleanup === 'function') this._cleanup();
|
||||||
|
this._listeners.forEach(({ el, event, handler }) => el.removeEventListener(event, handler));
|
||||||
|
this._listeners = [];
|
||||||
|
registry._unregister(this.id);
|
||||||
|
};
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
// ══════════════════════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════════════════════
|
||||||
// §1 PROVIDER REGISTRY
|
// §1 PROVIDER REGISTRY
|
||||||
// ══════════════════════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════════════════════
|
||||||
@@ -933,3 +994,98 @@ function updateTabArrows(tabs) {
|
|||||||
left.classList.toggle('visible', !atStart);
|
left.classList.toggle('visible', !atStart);
|
||||||
right.classList.toggle('visible', !atEnd);
|
right.classList.toggle('visible', !atEnd);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
// §9 THEME
|
||||||
|
// ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
const Theme = {
|
||||||
|
_key: 'switchboard_theme',
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.set(localStorage.getItem(this._key) || 'system');
|
||||||
|
},
|
||||||
|
|
||||||
|
set(mode) {
|
||||||
|
localStorage.setItem(this._key, mode);
|
||||||
|
if (mode === 'system') document.documentElement.removeAttribute('data-theme');
|
||||||
|
else document.documentElement.setAttribute('data-theme', mode);
|
||||||
|
},
|
||||||
|
|
||||||
|
get() {
|
||||||
|
return localStorage.getItem(this._key) || 'system';
|
||||||
|
},
|
||||||
|
|
||||||
|
resolved() {
|
||||||
|
const mode = this.get();
|
||||||
|
if (mode !== 'system') return mode;
|
||||||
|
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||||
|
},
|
||||||
|
|
||||||
|
renderToggle(containerId) {
|
||||||
|
const el = document.getElementById(containerId);
|
||||||
|
if (!el) return;
|
||||||
|
const current = this.get();
|
||||||
|
const options = [
|
||||||
|
{ id: 'light', label: 'Light', icon: '☀' },
|
||||||
|
{ id: 'dark', label: 'Dark', icon: '☾' },
|
||||||
|
{ id: 'system',label: 'System',icon: '⊞' },
|
||||||
|
];
|
||||||
|
el.innerHTML = '<div class="toggle-group">' + options.map(o =>
|
||||||
|
`<button class="toggle-btn${current === o.id ? ' active' : ''}" data-theme="${o.id}" title="${o.label}">${o.icon}</button>`
|
||||||
|
).join('') + '</div>';
|
||||||
|
el.querySelectorAll('.toggle-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
this.set(btn.dataset.theme);
|
||||||
|
this.renderToggle(containerId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
// §10 AVATAR UPLOAD
|
||||||
|
// ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mount an avatar upload component into a container.
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} containerEl
|
||||||
|
* @param {Object} opts
|
||||||
|
* @param {string} [opts.current] - Current avatar URL
|
||||||
|
* @param {number} [opts.size=80] - Avatar size in px
|
||||||
|
* @param {Function} [opts.onUpload] - Called with (file, dataURL)
|
||||||
|
* @returns {{ setImage }}
|
||||||
|
*/
|
||||||
|
function mountAvatarUpload(containerEl, opts = {}) {
|
||||||
|
const size = opts.size || 80;
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'file';
|
||||||
|
input.accept = 'image/*';
|
||||||
|
input.style.display = 'none';
|
||||||
|
|
||||||
|
containerEl.className = 'avatar-upload';
|
||||||
|
containerEl.style.width = size + 'px';
|
||||||
|
containerEl.style.height = size + 'px';
|
||||||
|
|
||||||
|
function render(src) {
|
||||||
|
if (src) containerEl.innerHTML = `<img src="${esc(src)}" alt="Avatar">`;
|
||||||
|
else containerEl.innerHTML = '<div class="avatar-upload__placeholder">Upload<br>Photo</div>';
|
||||||
|
containerEl.appendChild(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(opts.current || null);
|
||||||
|
containerEl.addEventListener('click', () => input.click());
|
||||||
|
input.addEventListener('change', () => {
|
||||||
|
const file = input.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (ev) => {
|
||||||
|
render(ev.target.result);
|
||||||
|
if (opts.onUpload) opts.onUpload(file, ev.target.result);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
});
|
||||||
|
|
||||||
|
return { setImage: render };
|
||||||
|
}
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ Object.assign(UI, {
|
|||||||
if (scaleEl) prefs.scale = parseInt(scaleEl.value);
|
if (scaleEl) prefs.scale = parseInt(scaleEl.value);
|
||||||
if (msgFontEl) prefs.msgFont = parseInt(msgFontEl.value);
|
if (msgFontEl) prefs.msgFont = parseInt(msgFontEl.value);
|
||||||
// v0.23.2: Theme saved via Theme API (localStorage key: switchboard_theme)
|
// v0.23.2: Theme saved via Theme API (localStorage key: switchboard_theme)
|
||||||
const activeThemeBtn = document.querySelector('#themeToggle .theme-btn.active');
|
const activeThemeBtn = document.querySelector('#themeToggle .toggle-btn.active');
|
||||||
if (activeThemeBtn && typeof Theme !== 'undefined') {
|
if (activeThemeBtn && typeof Theme !== 'undefined') {
|
||||||
Theme.set(activeThemeBtn.dataset.theme);
|
Theme.set(activeThemeBtn.dataset.theme);
|
||||||
}
|
}
|
||||||
@@ -156,17 +156,17 @@ Object.assign(UI, {
|
|||||||
if (msgFontEl) { msgFontEl.value = msgFont; document.getElementById('msgFontValue').textContent = msgFont + 'px'; }
|
if (msgFontEl) { msgFontEl.value = msgFont; document.getElementById('msgFontValue').textContent = msgFont + 'px'; }
|
||||||
|
|
||||||
// Highlight active theme button and wire click
|
// Highlight active theme button and wire click
|
||||||
document.querySelectorAll('#themeToggle .theme-btn').forEach(btn => {
|
document.querySelectorAll('#themeToggle .toggle-btn').forEach(btn => {
|
||||||
btn.classList.toggle('active', btn.dataset.theme === theme);
|
btn.classList.toggle('active', btn.dataset.theme === theme);
|
||||||
btn.onclick = () => {
|
btn.onclick = () => {
|
||||||
document.querySelectorAll('#themeToggle .theme-btn').forEach(b => b.classList.remove('active'));
|
document.querySelectorAll('#themeToggle .toggle-btn').forEach(b => b.classList.remove('active'));
|
||||||
btn.classList.add('active');
|
btn.classList.add('active');
|
||||||
if (typeof Theme !== 'undefined') Theme.set(btn.dataset.theme);
|
if (typeof Theme !== 'undefined') Theme.set(btn.dataset.theme);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// Highlight active keymap button
|
// Highlight active keymap button
|
||||||
document.querySelectorAll('#keymapToggle .theme-btn').forEach(btn => {
|
document.querySelectorAll('#keymapToggle .toggle-btn').forEach(btn => {
|
||||||
btn.classList.toggle('active', btn.dataset.keymap === keymap);
|
btn.classList.toggle('active', btn.dataset.keymap === keymap);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -192,11 +192,11 @@ Object.assign(UI, {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Theme toggle buttons
|
// Theme toggle buttons
|
||||||
document.querySelectorAll('#themeToggle .theme-btn').forEach(btn => {
|
document.querySelectorAll('#themeToggle .toggle-btn').forEach(btn => {
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
const theme = btn.dataset.theme;
|
const theme = btn.dataset.theme;
|
||||||
UI.applyTheme(theme);
|
UI.applyTheme(theme);
|
||||||
document.querySelectorAll('#themeToggle .theme-btn').forEach(b =>
|
document.querySelectorAll('#themeToggle .toggle-btn').forEach(b =>
|
||||||
b.classList.toggle('active', b === btn)
|
b.classList.toggle('active', b === btn)
|
||||||
);
|
);
|
||||||
const p = JSON.parse(localStorage.getItem('cs-appearance') || '{}');
|
const p = JSON.parse(localStorage.getItem('cs-appearance') || '{}');
|
||||||
@@ -206,10 +206,10 @@ Object.assign(UI, {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Editor keymap toggle buttons
|
// Editor keymap toggle buttons
|
||||||
document.querySelectorAll('#keymapToggle .theme-btn').forEach(btn => {
|
document.querySelectorAll('#keymapToggle .toggle-btn').forEach(btn => {
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
const mode = btn.dataset.keymap;
|
const mode = btn.dataset.keymap;
|
||||||
document.querySelectorAll('#keymapToggle .theme-btn').forEach(b =>
|
document.querySelectorAll('#keymapToggle .toggle-btn').forEach(b =>
|
||||||
b.classList.toggle('active', b === btn)
|
b.classList.toggle('active', b === btn)
|
||||||
);
|
);
|
||||||
// Persist
|
// Persist
|
||||||
@@ -395,7 +395,7 @@ Object.assign(UI, {
|
|||||||
</select>
|
</select>
|
||||||
</td>
|
</td>
|
||||||
<td class="admin-actions-cell">
|
<td class="admin-actions-cell">
|
||||||
<button class="btn-icon btn-icon-danger" onclick="settingsRemoveTeamMember('${teamId}','${m.id}','${esc(m.email)}')" title="Remove">🗑</button>
|
<button class="icon-btn icon-btn-danger" onclick="settingsRemoveTeamMember('${teamId}','${m.id}','${esc(m.email)}')" title="Remove">🗑</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>`).join('')}</tbody>
|
</tr>`).join('')}</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -12,12 +12,11 @@
|
|||||||
// menu.destroy();
|
// menu.destroy();
|
||||||
|
|
||||||
const UserMenu = {
|
const UserMenu = {
|
||||||
primary: null,
|
...createComponentRegistry('UserMenu'),
|
||||||
_instances: new Map(),
|
|
||||||
|
|
||||||
create(opts) {
|
create(opts) {
|
||||||
const pfx = opts.id || '';
|
const pfx = opts.id || '';
|
||||||
const instance = {
|
const instance = componentMixin({
|
||||||
id: pfx,
|
id: pfx,
|
||||||
btnEl: document.getElementById(pfx + 'userMenuBtn'),
|
btnEl: document.getElementById(pfx + 'userMenuBtn'),
|
||||||
avatarEl: document.getElementById(pfx + 'userAvatar'),
|
avatarEl: document.getElementById(pfx + 'userAvatar'),
|
||||||
@@ -30,7 +29,6 @@ const UserMenu = {
|
|||||||
debugEl: document.getElementById(pfx + 'menuDebug'),
|
debugEl: document.getElementById(pfx + 'menuDebug'),
|
||||||
signoutEl: document.getElementById(pfx + 'menuSignout'),
|
signoutEl: document.getElementById(pfx + 'menuSignout'),
|
||||||
_open: false,
|
_open: false,
|
||||||
_listeners: [],
|
|
||||||
|
|
||||||
// ── User display ────────────────────
|
// ── User display ────────────────────
|
||||||
|
|
||||||
@@ -127,28 +125,9 @@ const UserMenu = {
|
|||||||
this._on(this.debugEl, 'click', () => { this.close(); if (handlers.onDebug) handlers.onDebug(); });
|
this._on(this.debugEl, 'click', () => { this.close(); if (handlers.onDebug) handlers.onDebug(); });
|
||||||
this._on(this.signoutEl, 'click', () => { this.close(); if (handlers.onSignout) handlers.onSignout(); });
|
this._on(this.signoutEl, 'click', () => { this.close(); if (handlers.onSignout) handlers.onSignout(); });
|
||||||
},
|
},
|
||||||
|
}, UserMenu);
|
||||||
|
|
||||||
// ── Lifecycle ───────────────────────
|
UserMenu._register(pfx, instance);
|
||||||
|
|
||||||
destroy() {
|
|
||||||
this._listeners.forEach(({ el, event, handler }) => el.removeEventListener(event, handler));
|
|
||||||
this._listeners = [];
|
|
||||||
UserMenu._instances.delete(this.id);
|
|
||||||
if (UserMenu.primary === this) UserMenu.primary = null;
|
|
||||||
},
|
|
||||||
|
|
||||||
// ── Internal ────────────────────────
|
|
||||||
|
|
||||||
_on(el, event, handler) {
|
|
||||||
if (!el) return;
|
|
||||||
el.addEventListener(event, handler);
|
|
||||||
this._listeners.push({ el, event, handler });
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
UserMenu._instances.set(pfx, instance);
|
|
||||||
return instance;
|
return instance;
|
||||||
},
|
},
|
||||||
|
|
||||||
get(id) { return this._instances.get(id) || null; },
|
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user