Changeset 0.25.0 (#160)
This commit is contained in:
@@ -77,6 +77,11 @@ type RoleSelection struct {
|
||||
}
|
||||
|
||||
// ── Page data structs ────────────────────────
|
||||
//
|
||||
// v0.25.0: Each loader function is a "data provider" keyed by surface ID.
|
||||
// The surface manifest's DataRequires field references these keys.
|
||||
// Currently 1:1 (one loader per surface). Future: composite loaders
|
||||
// that assemble data from multiple providers for dashboard-style surfaces.
|
||||
|
||||
// AdminPageData is what the admin surface templates receive.
|
||||
type AdminPageData struct {
|
||||
@@ -129,6 +134,16 @@ func (e *Engine) registerLoaders() {
|
||||
e.RegisterLoader("settings", e.settingsLoader)
|
||||
}
|
||||
|
||||
// ListDataProviders returns the keys of all registered data providers.
|
||||
// Used for manifest validation — DataRequires entries must match a key here.
|
||||
func (e *Engine) ListDataProviders() []string {
|
||||
keys := make([]string, 0, len(e.loaders))
|
||||
for k := range e.loaders {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
// ── Admin loader ─────────────────────────────
|
||||
// Pre-loads ALL dropdown data. Fixes bugs #1 (model roles) and #2 (team scope).
|
||||
|
||||
@@ -299,7 +314,7 @@ func sectionCategory(section string) string {
|
||||
return "ai"
|
||||
case "health", "routing", "capabilities":
|
||||
return "routing"
|
||||
case "settings", "storage", "extensions":
|
||||
case "settings", "storage", "extensions", "channels", "surfaces":
|
||||
return "system"
|
||||
case "usage", "audit", "stats":
|
||||
return "monitoring"
|
||||
|
||||
@@ -39,9 +39,28 @@ type Engine struct {
|
||||
cfg *config.Config
|
||||
stores store.Stores
|
||||
loaders map[string]DataLoaderFunc
|
||||
surfaces []SurfaceManifest // v0.25.0: registered surface definitions
|
||||
devMode bool
|
||||
}
|
||||
|
||||
// SurfaceManifest describes a surface's registration. Core surfaces are
|
||||
// registered in Go at startup. Extension surfaces will be registered from
|
||||
// manifest files (future).
|
||||
type SurfaceManifest struct {
|
||||
ID string `json:"id"` // unique identifier: "chat", "editor", "my-dashboard"
|
||||
Route string `json:"route"` // primary URL pattern: "/", "/editor/:wsId"
|
||||
AltRoutes []string `json:"alt_routes"` // additional URL patterns: ["/chat/:chatID"]
|
||||
Title string `json:"title"` // human-readable: "Chat", "Editor"
|
||||
Template string `json:"template"` // Go template name: "surface-chat", "surface-editor"
|
||||
Components []string `json:"components"` // component IDs used: ["chat-pane", "file-tree"]
|
||||
DataRequires []string `json:"data_requires"` // data loader keys: ["workspace", "models"]
|
||||
Scripts []string `json:"scripts"` // JS files (surface-specific, beyond base.html common)
|
||||
Styles []string `json:"styles"` // CSS files (surface-specific)
|
||||
Auth string `json:"auth"` // "authenticated", "public", "session", "admin"
|
||||
Layout string `json:"layout"` // default pane layout preset: "single", "editor"
|
||||
Source string `json:"source"` // "core" or "extension"
|
||||
}
|
||||
|
||||
// BannerConfig holds environment banner settings.
|
||||
type BannerConfig struct {
|
||||
Text string `json:"text"`
|
||||
@@ -66,6 +85,12 @@ type PageData struct {
|
||||
Theme string // "dark", "light", or "" (= use default)
|
||||
SurfaceSettings any // JSON-serialized to window.__SETTINGS__
|
||||
|
||||
// v0.25.0: Surface manifest — layout preset, components, etc.
|
||||
Manifest *SurfaceManifest `json:"manifest,omitempty"`
|
||||
|
||||
// v0.25.0: Enabled surface IDs for conditional nav rendering.
|
||||
EnabledSurfaces []string `json:"-"`
|
||||
|
||||
// v0.22.7: Login/splash page fields
|
||||
InstanceName string // branding: instance display name
|
||||
LogoURL string // branding: custom logo URL
|
||||
@@ -74,6 +99,17 @@ type PageData struct {
|
||||
AuthMode string // v0.24.1: "builtin", "mtls", "oidc"
|
||||
}
|
||||
|
||||
// SurfaceEnabled returns true if the given surface ID is in the EnabledSurfaces list.
|
||||
// Used by templates: {{if .SurfaceEnabled "editor"}}
|
||||
func (pd PageData) SurfaceEnabled(id string) bool {
|
||||
for _, s := range pd.EnabledSurfaces {
|
||||
if s == id {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// UserContext is the authenticated user's info available to templates.
|
||||
type UserContext struct {
|
||||
ID string `json:"id"`
|
||||
@@ -96,9 +132,74 @@ func New(cfg *config.Config, stores store.Stores) *Engine {
|
||||
}
|
||||
e.parseTemplates()
|
||||
e.registerLoaders()
|
||||
e.registerCoreSurfaces()
|
||||
e.SeedSurfaces() // v0.25.0: Write core manifests to DB (preserves admin toggle state)
|
||||
return e
|
||||
}
|
||||
|
||||
// registerCoreSurfaces populates the surface manifest list with the five
|
||||
// core surfaces. Extension surfaces will be appended at startup from
|
||||
// extension manifests (future).
|
||||
func (e *Engine) registerCoreSurfaces() {
|
||||
e.surfaces = []SurfaceManifest{
|
||||
{
|
||||
ID: "chat", Route: "/", AltRoutes: []string{"/chat/:chatID"},
|
||||
Title: "Chat", Template: "surface-chat", Auth: "authenticated",
|
||||
Components: []string{"chat-pane", "model-selector", "user-menu"},
|
||||
DataRequires: []string{"chat"},
|
||||
Layout: "single", Source: "core",
|
||||
},
|
||||
{
|
||||
ID: "editor", Route: "/editor/:wsId", AltRoutes: []string{"/editor"},
|
||||
Title: "Editor", Template: "surface-editor", Auth: "authenticated",
|
||||
Components: []string{"file-tree", "code-editor", "chat-pane", "note-editor", "model-selector"},
|
||||
DataRequires: []string{"editor"},
|
||||
Layout: "editor", Source: "extension", // Dogfood: editor is the first non-core surface
|
||||
},
|
||||
{
|
||||
ID: "notes", Route: "/notes", AltRoutes: []string{"/notes/:noteId"},
|
||||
Title: "Notes", Template: "surface-notes", Auth: "authenticated",
|
||||
Components: []string{"note-editor", "chat-pane"},
|
||||
DataRequires: []string{"notes"},
|
||||
Layout: "single", Source: "core",
|
||||
},
|
||||
{
|
||||
ID: "admin", Route: "/admin/:section", AltRoutes: []string{"/admin"},
|
||||
Title: "Admin", Template: "surface-admin", Auth: "admin",
|
||||
DataRequires: []string{"admin"},
|
||||
Layout: "single", Source: "core",
|
||||
},
|
||||
{
|
||||
ID: "settings", Route: "/settings/:section", AltRoutes: []string{"/settings"},
|
||||
Title: "Settings", Template: "surface-settings", Auth: "authenticated",
|
||||
DataRequires: []string{"settings"},
|
||||
Layout: "single", Source: "core",
|
||||
},
|
||||
{
|
||||
ID: "workflow", Route: "/w/:id",
|
||||
Title: "Workflow", Template: "workflow", Auth: "session",
|
||||
Components: []string{"chat-pane"},
|
||||
DataRequires: []string{"workflow"},
|
||||
Layout: "single", Source: "core",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// GetSurface returns the manifest for a surface by ID, or nil if not found.
|
||||
func (e *Engine) GetSurface(id string) *SurfaceManifest {
|
||||
for i := range e.surfaces {
|
||||
if e.surfaces[i].ID == id {
|
||||
return &e.surfaces[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Surfaces returns all registered surface manifests.
|
||||
func (e *Engine) Surfaces() []SurfaceManifest {
|
||||
return e.surfaces
|
||||
}
|
||||
|
||||
// parseTemplates compiles all embedded templates with the custom FuncMap.
|
||||
func (e *Engine) parseTemplates() {
|
||||
funcMap := template.FuncMap{
|
||||
@@ -191,14 +292,156 @@ func (e *Engine) RenderSurface(surfaceID string) gin.HandlerFunc {
|
||||
}
|
||||
|
||||
e.Render(c, "base.html", PageData{
|
||||
Surface: surfaceID,
|
||||
Section: section,
|
||||
User: user,
|
||||
Data: data,
|
||||
Surface: surfaceID,
|
||||
Section: section,
|
||||
User: user,
|
||||
Data: data,
|
||||
Manifest: e.GetSurface(surfaceID),
|
||||
EnabledSurfaces: e.EnabledSurfaceIDs(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// PageRouteMiddleware holds middleware handlers for each auth level.
|
||||
// Passed to RegisterPageRoutes by main.go.
|
||||
type PageRouteMiddleware struct {
|
||||
Authenticated gin.HandlerFunc // AuthOrRedirect — for chat, editor, notes, settings
|
||||
Admin []gin.HandlerFunc // AuthOrRedirect + RequireAdminPage
|
||||
Session gin.HandlerFunc // AuthOrSession — for workflow
|
||||
}
|
||||
|
||||
// RegisterPageRoutes generates Gin routes from the registered surface manifests.
|
||||
// Replaces manual per-surface route blocks in main.go.
|
||||
//
|
||||
// Groups surfaces by auth level to avoid redundant middleware chains.
|
||||
// Workflow surfaces use RenderWorkflow() (separate template + data model).
|
||||
// All other surfaces use RenderSurface().
|
||||
func (e *Engine) RegisterPageRoutes(base *gin.RouterGroup, mw PageRouteMiddleware) {
|
||||
// Group surfaces by auth type
|
||||
byAuth := map[string][]SurfaceManifest{}
|
||||
for _, s := range e.surfaces {
|
||||
byAuth[s.Auth] = append(byAuth[s.Auth], s)
|
||||
}
|
||||
|
||||
// Authenticated surfaces — single middleware group
|
||||
if surfaces, ok := byAuth["authenticated"]; ok && mw.Authenticated != nil {
|
||||
group := base.Group("")
|
||||
group.Use(mw.Authenticated)
|
||||
for _, s := range surfaces {
|
||||
if !e.IsSurfaceEnabled(s.ID) {
|
||||
// Disabled surface → redirect to chat
|
||||
registerRoutes(group, s, e.disabledRedirect())
|
||||
continue
|
||||
}
|
||||
handler := e.RenderSurface(s.ID)
|
||||
registerRoutes(group, s, handler)
|
||||
}
|
||||
}
|
||||
|
||||
// Admin surfaces — auth + admin role middleware
|
||||
if surfaces, ok := byAuth["admin"]; ok && len(mw.Admin) > 0 {
|
||||
for _, s := range surfaces {
|
||||
prefix := routePrefix(s.Route)
|
||||
group := base.Group(prefix)
|
||||
for _, m := range mw.Admin {
|
||||
group.Use(m)
|
||||
}
|
||||
// Admin surfaces are always enabled (needed to manage other surfaces)
|
||||
handler := e.RenderSurface(s.ID)
|
||||
registerRoutesRelative(group, prefix, s, handler)
|
||||
}
|
||||
}
|
||||
|
||||
// Session surfaces (workflow) — session middleware
|
||||
if surfaces, ok := byAuth["session"]; ok && mw.Session != nil {
|
||||
for _, s := range surfaces {
|
||||
if !e.IsSurfaceEnabled(s.ID) {
|
||||
prefix := routePrefix(s.Route)
|
||||
group := base.Group(prefix)
|
||||
group.Use(mw.Session)
|
||||
registerRoutesRelative(group, prefix, s, e.disabledRedirect())
|
||||
continue
|
||||
}
|
||||
prefix := routePrefix(s.Route)
|
||||
group := base.Group(prefix)
|
||||
group.Use(mw.Session)
|
||||
handler := e.surfaceHandler(s)
|
||||
registerRoutesRelative(group, prefix, s, handler)
|
||||
}
|
||||
}
|
||||
|
||||
// Public surfaces — no middleware
|
||||
if surfaces, ok := byAuth["public"]; ok {
|
||||
for _, s := range surfaces {
|
||||
if !e.IsSurfaceEnabled(s.ID) {
|
||||
registerRoutes(base, s, e.disabledRedirect())
|
||||
continue
|
||||
}
|
||||
handler := e.RenderSurface(s.ID)
|
||||
registerRoutes(base, s, handler)
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("[pages] Registered routes for %d surfaces", len(e.surfaces))
|
||||
}
|
||||
|
||||
// surfaceHandler returns the appropriate Gin handler for a surface.
|
||||
func (e *Engine) surfaceHandler(s SurfaceManifest) gin.HandlerFunc {
|
||||
if s.ID == "workflow" {
|
||||
return e.RenderWorkflow()
|
||||
}
|
||||
return e.RenderSurface(s.ID)
|
||||
}
|
||||
|
||||
// registerRoutes adds the primary route and alt routes for a surface.
|
||||
// Used for surfaces in the root group (authenticated).
|
||||
func registerRoutes(group *gin.RouterGroup, s SurfaceManifest, handler gin.HandlerFunc) {
|
||||
group.GET(s.Route, handler)
|
||||
for _, alt := range s.AltRoutes {
|
||||
if alt != s.Route {
|
||||
group.GET(alt, handler)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// registerRoutesRelative adds routes relative to a group prefix.
|
||||
// Used for admin/session surfaces with their own prefix group.
|
||||
// "/admin/:section" with prefix "/admin" → "/:section"
|
||||
func registerRoutesRelative(group *gin.RouterGroup, prefix string, s SurfaceManifest, handler gin.HandlerFunc) {
|
||||
rel := stripPrefix(s.Route, prefix)
|
||||
group.GET(rel, handler)
|
||||
for _, alt := range s.AltRoutes {
|
||||
r := stripPrefix(alt, prefix)
|
||||
if r != rel {
|
||||
group.GET(r, handler)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// routePrefix extracts the first path segment as a group prefix.
|
||||
// "/admin/:section" → "/admin", "/w/:id" → "/w", "/" → ""
|
||||
func routePrefix(route string) string {
|
||||
trimmed := strings.TrimPrefix(route, "/")
|
||||
parts := strings.SplitN(trimmed, "/", 2)
|
||||
if len(parts) == 0 || parts[0] == "" || strings.HasPrefix(parts[0], ":") {
|
||||
return ""
|
||||
}
|
||||
return "/" + parts[0]
|
||||
}
|
||||
|
||||
// stripPrefix removes a prefix from a route path. Returns "" if the
|
||||
// entire path IS the prefix (which Gin interprets as the group root).
|
||||
func stripPrefix(route, prefix string) string {
|
||||
if prefix == "" {
|
||||
return route
|
||||
}
|
||||
rel := strings.TrimPrefix(route, prefix)
|
||||
if rel == "" {
|
||||
rel = ""
|
||||
}
|
||||
return rel
|
||||
}
|
||||
|
||||
// RenderLogin serves the standalone login page.
|
||||
func (e *Engine) RenderLogin() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
|
||||
70
server/pages/pages_surfaces.go
Normal file
70
server/pages/pages_surfaces.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// SeedSurfaces writes core surface manifests to the registry table.
|
||||
// Called once at startup. Does NOT overwrite the enabled flag — admin
|
||||
// toggles survive restarts.
|
||||
func (e *Engine) SeedSurfaces() {
|
||||
ctx := context.Background()
|
||||
for _, s := range e.surfaces {
|
||||
manifest := map[string]any{
|
||||
"route": s.Route,
|
||||
"alt_routes": s.AltRoutes,
|
||||
"template": s.Template,
|
||||
"components": s.Components,
|
||||
"data_requires": s.DataRequires,
|
||||
"auth": s.Auth,
|
||||
"layout": s.Layout,
|
||||
}
|
||||
if err := e.stores.Surfaces.Seed(ctx, s.ID, s.Title, s.Source, manifest); err != nil {
|
||||
log.Printf("[pages] Failed to seed surface %q: %v", s.ID, err)
|
||||
}
|
||||
}
|
||||
log.Printf("[pages] Seeded %d core surfaces into registry", len(e.surfaces))
|
||||
}
|
||||
|
||||
// IsSurfaceEnabled checks if a surface is enabled in the registry.
|
||||
// Chat and Admin are always enabled (system-critical).
|
||||
// Returns true if the surface is not found (fail-open for backward compat).
|
||||
func (e *Engine) IsSurfaceEnabled(surfaceID string) bool {
|
||||
// Chat and Admin cannot be disabled — they're system-critical
|
||||
if surfaceID == "chat" || surfaceID == "admin" {
|
||||
return true
|
||||
}
|
||||
ctx := context.Background()
|
||||
sr, err := e.stores.Surfaces.Get(ctx, surfaceID)
|
||||
if err != nil || sr == nil {
|
||||
return true // Not in registry = assume enabled (backward compat)
|
||||
}
|
||||
return sr.Enabled
|
||||
}
|
||||
|
||||
// EnabledSurfaceIDs returns a list of enabled surface IDs for nav rendering.
|
||||
func (e *Engine) EnabledSurfaceIDs() []string {
|
||||
ctx := context.Background()
|
||||
ids, err := e.stores.Surfaces.ListEnabled(ctx)
|
||||
if err != nil {
|
||||
log.Printf("[pages] Failed to load enabled surfaces: %v", err)
|
||||
// Fallback: return all core surface IDs
|
||||
var all []string
|
||||
for _, s := range e.surfaces {
|
||||
all = append(all, s.ID)
|
||||
}
|
||||
return all
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
// disabledRedirect returns a handler that redirects to the chat surface.
|
||||
func (e *Engine) disabledRedirect() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Redirect(http.StatusTemporaryRedirect, e.cfg.BasePath+"/")
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,11 @@
|
||||
<link rel="stylesheet" href="{{.BasePath}}/css/panels.css?v={{.Version}}">
|
||||
<link rel="stylesheet" href="{{.BasePath}}/css/surfaces.css?v={{.Version}}">
|
||||
<link rel="stylesheet" href="{{.BasePath}}/css/splash.css?v={{.Version}}">
|
||||
<link rel="stylesheet" href="{{.BasePath}}/css/pane-container.css?v={{.Version}}">
|
||||
<link rel="stylesheet" href="{{.BasePath}}/css/chat-pane.css?v={{.Version}}">
|
||||
<link rel="stylesheet" href="{{.BasePath}}/css/user-menu.css?v={{.Version}}">
|
||||
<link rel="stylesheet" href="{{.BasePath}}/css/tool-grants.css?v={{.Version}}">
|
||||
<link rel="stylesheet" href="{{.BasePath}}/css/admin-surfaces.css?v={{.Version}}">
|
||||
{{if eq .Surface "chat"}}{{template "css-chat" .}}{{end}}
|
||||
{{if eq .Surface "editor"}}{{template "css-editor" .}}{{end}}
|
||||
<style>
|
||||
@@ -74,6 +79,7 @@
|
||||
window.__SURFACE__ = '{{.Surface}}';
|
||||
window.__PAGE_DATA__ = {{.Data | toJSON}};
|
||||
window.__USER__ = {{.User | toJSON}};
|
||||
{{if .Manifest}}window.__MANIFEST__ = {{.Manifest | toJSON}};{{end}}
|
||||
</script>
|
||||
|
||||
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/app-state.js?v={{.Version}}"></script>
|
||||
@@ -87,6 +93,13 @@
|
||||
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/ui-primitives-additions.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>
|
||||
{{/* v0.25.0: Component scripts — available on all surfaces */}}
|
||||
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/user-menu.js?v={{.Version}}"></script>
|
||||
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/model-selector.js?v={{.Version}}"></script>
|
||||
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/file-tree.js?v={{.Version}}"></script>
|
||||
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/code-editor.js?v={{.Version}}"></script>
|
||||
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/note-editor.js?v={{.Version}}"></script>
|
||||
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/pane-container.js?v={{.Version}}"></script>
|
||||
|
||||
{{if eq .Surface "chat"}}{{template "scripts-chat" .}}{{end}}
|
||||
{{if eq .Surface "admin"}}{{template "scripts-admin" .}}{{end}}
|
||||
|
||||
@@ -3,12 +3,27 @@
|
||||
Usage: {{template "chat-pane" dict "ID" "main"}}
|
||||
Creates mount points: {ID}ChatMessages, {ID}ChatInput, {ID}SendBtn, {ID}ModelSel
|
||||
ChatPane.create() in chat-pane.js binds to these IDs.
|
||||
|
||||
The header bar ({ID}ChatHeader) is hidden by default.
|
||||
Standalone panes (editor assist) show it for chat switching + model selection.
|
||||
*/}}
|
||||
{{define "chat-pane"}}
|
||||
<div class="chat-pane" id="{{.ID}}ChatPane">
|
||||
<div class="chat-pane-header" id="{{.ID}}ChatHeader" style="display:none;">
|
||||
<div class="chat-pane-header-left">
|
||||
<select class="chat-pane-chat-select" id="{{.ID}}ChatSelect" title="Switch chat">
|
||||
<option value="">New conversation</option>
|
||||
</select>
|
||||
<button class="chat-pane-new-btn" id="{{.ID}}ChatNewBtn" title="New chat">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="chat-pane-header-right">
|
||||
<div class="chat-pane-model-sel" id="{{.ID}}ModelSel"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-pane-messages" id="{{.ID}}ChatMessages"></div>
|
||||
<div class="chat-pane-input-bar">
|
||||
<div class="chat-pane-model-sel" id="{{.ID}}ModelSel"></div>
|
||||
<div class="chat-pane-input-wrap">
|
||||
<div class="chat-pane-input" id="{{.ID}}ChatInput"></div>
|
||||
<button class="chat-pane-send" id="{{.ID}}SendBtn" title="Send">
|
||||
|
||||
30
server/pages/templates/components/code-editor.html
Normal file
30
server/pages/templates/components/code-editor.html
Normal file
@@ -0,0 +1,30 @@
|
||||
{{/*
|
||||
Code Editor Component — tabbed CM6 editor with tab bar, save, and status bar.
|
||||
Usage: {{template "code-editor" dict "ID" "editor"}}
|
||||
Creates mount points: {ID}EditorTabs, {ID}EditorContent, {ID}EditorWelcome,
|
||||
{ID}EditorStatus, {ID}EditorStatusFile, {ID}EditorStatusLang, {ID}EditorStatusBranch.
|
||||
CodeEditor.create() in code-editor.js binds to these IDs.
|
||||
*/}}
|
||||
{{define "code-editor"}}
|
||||
<div class="code-editor" id="{{.ID}}CodeEditor">
|
||||
<div class="code-editor-tabs" id="{{.ID}}EditorTabs">
|
||||
{{/* Populated by CodeEditor.openFile() */}}
|
||||
</div>
|
||||
<div class="code-editor-content" id="{{.ID}}EditorContent">
|
||||
<div class="code-editor-welcome" id="{{.ID}}EditorWelcome">
|
||||
<div style="display:flex;align-items:center;justify-content:center;height:100%;color:var(--text-3);">
|
||||
<div style="text-align:center;">
|
||||
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" style="opacity:0.12;display:inline-block;margin-bottom:10px;"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><polyline points="13 2 13 9 20 9"/></svg>
|
||||
<p style="margin:0;font-size:14px;">Open a file to start editing</p>
|
||||
<p style="margin:6px 0 0;font-size:12px;">Select from the file tree or Ctrl+P</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="code-editor-statusbar" id="{{.ID}}EditorStatus">
|
||||
<span id="{{.ID}}EditorStatusFile"></span>
|
||||
<span id="{{.ID}}EditorStatusLang"></span>
|
||||
<span id="{{.ID}}EditorStatusBranch"></span>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
20
server/pages/templates/components/file-tree.html
Normal file
20
server/pages/templates/components/file-tree.html
Normal file
@@ -0,0 +1,20 @@
|
||||
{{/*
|
||||
File Tree Component — workspace file browser with directory expand/collapse.
|
||||
Usage: {{template "file-tree" dict "ID" "editor"}}
|
||||
Creates mount points: {ID}FileTree, {ID}TreeHeader, {ID}TreeItems, {ID}TreeNewFile.
|
||||
FileTree.create() in file-tree.js binds to these IDs.
|
||||
*/}}
|
||||
{{define "file-tree"}}
|
||||
<div class="file-tree" id="{{.ID}}FileTree">
|
||||
<div class="file-tree-header" id="{{.ID}}TreeHeader">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
|
||||
<span class="file-tree-title">Files</span>
|
||||
<button class="icon-btn" id="{{.ID}}TreeNewFile" title="New file">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="file-tree-items" id="{{.ID}}TreeItems">
|
||||
{{/* Populated by FileTree.refresh() */}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
19
server/pages/templates/components/model-selector.html
Normal file
19
server/pages/templates/components/model-selector.html
Normal file
@@ -0,0 +1,19 @@
|
||||
{{/*
|
||||
Model Selector Component — chat model + persona dropdown with capability badges.
|
||||
Usage: {{template "model-selector" dict "ID" ""}}
|
||||
With empty ID, generates existing IDs: modelDropdown, modelDropdownBtn, etc.
|
||||
With prefix: {{template "model-selector" dict "ID" "editor"}} → editorModelDropdown, etc.
|
||||
ModelSelector.create() in model-selector.js binds to these IDs.
|
||||
*/}}
|
||||
{{define "model-selector"}}
|
||||
<div id="{{.ID}}modelDropdown" class="model-dropdown">
|
||||
<button id="{{.ID}}modelDropdownBtn" class="model-dropdown-btn">
|
||||
<span id="{{.ID}}modelDropdownLabel" class="model-dropdown-label">Select model…</span>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
|
||||
</button>
|
||||
<div id="{{.ID}}modelDropdownMenu" class="model-dropdown-menu">
|
||||
{{/* Populated by ModelSelector.update() */}}
|
||||
</div>
|
||||
</div>
|
||||
<span id="{{.ID}}modelCaps" class="model-caps"></span>
|
||||
{{end}}
|
||||
86
server/pages/templates/components/note-editor.html
Normal file
86
server/pages/templates/components/note-editor.html
Normal file
@@ -0,0 +1,86 @@
|
||||
{{/*
|
||||
Note Editor Component — notes list + editor/reader with folders, search, backlinks.
|
||||
Usage: {{template "note-editor" dict "ID" "main"}}
|
||||
Creates mount points for list view, editor view, and graph view.
|
||||
NoteEditor.create() in note-editor.js binds to these IDs.
|
||||
|
||||
When used in the notes surface (standalone), notes.js wires event listeners
|
||||
and integrates with PanelRegistry. When used in the editor surface (tabbed pane),
|
||||
NoteEditor.create() provides independent lifecycle and event wiring.
|
||||
*/}}
|
||||
{{define "note-editor"}}
|
||||
<div class="note-editor" id="{{.ID}}NoteEditor">
|
||||
{{/* ── List View ───────────────────────── */}}
|
||||
<div class="note-editor-list-view" id="{{.ID}}NotesListView">
|
||||
<div class="notes-toolbar">
|
||||
<button id="{{.ID}}NotesNewBtn" class="btn-small" title="New note">+ New</button>
|
||||
<button id="{{.ID}}NotesTodayBtn" class="btn-small" title="Open daily note">📅 Today</button>
|
||||
<button id="{{.ID}}NotesGraphBtn" class="btn-small" title="Note graph">🕸 Graph</button>
|
||||
<button id="{{.ID}}NotesSelectModeBtn" class="btn-small" title="Select mode">☑ Select</button>
|
||||
</div>
|
||||
<div class="notes-search-row">
|
||||
<input type="text" id="{{.ID}}NotesSearchInput" class="notes-search-input" placeholder="Search notes…" autocomplete="off">
|
||||
</div>
|
||||
<div class="notes-filter-row">
|
||||
<select id="{{.ID}}NotesFolderFilter" class="notes-filter-select"><option value="">All folders</option></select>
|
||||
<select id="{{.ID}}NotesSortSelect" class="notes-filter-select">
|
||||
<option value="updated_desc">Last updated</option>
|
||||
<option value="created_desc">Created</option>
|
||||
<option value="alpha">Alphabetical</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="{{.ID}}NotesList" class="notes-list"></div>
|
||||
<div id="{{.ID}}NotesSelectionBar" class="notes-selection-bar" style="display:none;">
|
||||
<label class="notes-select-all"><input type="checkbox" id="{{.ID}}NotesSelectAll"> All</label>
|
||||
<span id="{{.ID}}NotesSelectedCount">0</span> selected
|
||||
<button id="{{.ID}}NotesDeleteSelectedBtn" class="btn-small btn-danger">Delete</button>
|
||||
<button id="{{.ID}}NotesCancelSelectBtn" class="btn-small">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{/* ── Editor View ─────────────────────── */}}
|
||||
<div class="note-editor-editor-view" id="{{.ID}}NotesEditorView" style="display:none;">
|
||||
<div class="notes-editor-header">
|
||||
<button id="{{.ID}}NotesBackBtn" class="btn-small" title="Back to list">← Back</button>
|
||||
<div style="flex:1"></div>
|
||||
<button id="{{.ID}}NoteEditBtn" class="btn-small" style="display:none;">Edit</button>
|
||||
<button id="{{.ID}}NotePreviewBtn" class="btn-small" style="display:none;">Preview</button>
|
||||
<button id="{{.ID}}NoteCancelEditBtn" class="btn-small" style="display:none;">Cancel</button>
|
||||
<button id="{{.ID}}NoteDeleteBtn" class="btn-small btn-danger" style="display:none;">Delete</button>
|
||||
<button id="{{.ID}}NoteSaveBtn" class="btn-small btn-primary">Save</button>
|
||||
</div>
|
||||
|
||||
{{/* Edit mode */}}
|
||||
<div id="{{.ID}}NoteEditMode" class="notes-editor">
|
||||
<input type="text" id="{{.ID}}NoteEditorTitle" class="notes-title-input" placeholder="Note title">
|
||||
<div class="notes-meta-row">
|
||||
<input type="text" id="{{.ID}}NoteEditorFolder" class="notes-folder-input" placeholder="Folder (e.g. work/project)">
|
||||
<input type="text" id="{{.ID}}NoteEditorTags" class="notes-tags-input" placeholder="Tags (comma-separated)">
|
||||
</div>
|
||||
<div id="{{.ID}}NoteEditorContentContainer" class="notes-content-container">
|
||||
<textarea id="{{.ID}}NoteEditorContent" rows="12" class="notes-content-input" placeholder="Write your note in markdown…"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{/* Read mode */}}
|
||||
<div id="{{.ID}}NoteReadMode" style="display:none;">
|
||||
<div class="notes-read-view">
|
||||
<h3 id="{{.ID}}NoteReadTitle" class="note-read-title"></h3>
|
||||
<div id="{{.ID}}NoteReadMeta" class="note-read-meta"></div>
|
||||
<div id="{{.ID}}NoteReadContent" class="note-read-content msg-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{/* Backlinks */}}
|
||||
<div id="{{.ID}}NoteBacklinks" class="note-backlinks" style="display:none;">
|
||||
<div class="note-backlinks-header">
|
||||
Backlinks <span id="{{.ID}}NoteBacklinksCount" class="badge">0</span>
|
||||
</div>
|
||||
<div id="{{.ID}}NoteBacklinksList" class="note-backlinks-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{/* ── Graph View ──────────────────────── */}}
|
||||
<div class="note-editor-graph-view" id="{{.ID}}NotesGraphView" style="display:none;"></div>
|
||||
</div>
|
||||
{{end}}
|
||||
40
server/pages/templates/components/user-menu.html
Normal file
40
server/pages/templates/components/user-menu.html
Normal file
@@ -0,0 +1,40 @@
|
||||
{{/*
|
||||
User Menu Component — reusable user avatar + flyout dropdown.
|
||||
Usage: {{template "user-menu" dict "ID" ""}}
|
||||
With empty ID, generates existing IDs: userMenuBtn, userAvatar, etc.
|
||||
With prefix: {{template "user-menu" dict "ID" "editor"}} → editorUserMenuBtn, etc.
|
||||
UserMenu.create() in user-menu.js binds to these IDs.
|
||||
*/}}
|
||||
{{define "user-menu"}}
|
||||
<div class="user-menu-wrap" id="{{.ID}}userMenuWrap">
|
||||
<button id="{{.ID}}userMenuBtn" class="user-btn">
|
||||
<div id="{{.ID}}userAvatar" class="user-avatar">
|
||||
<span id="{{.ID}}avatarLetter">?</span>
|
||||
</div>
|
||||
<span id="{{.ID}}userName" class="sb-label">User</span>
|
||||
</button>
|
||||
<div id="{{.ID}}userFlyout" class="user-flyout">
|
||||
<button id="{{.ID}}menuSettings" class="flyout-item">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.32 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
|
||||
<span>Settings</span>
|
||||
</button>
|
||||
<button id="{{.ID}}menuAdmin" class="flyout-item" style="display:none;">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
|
||||
<span>Admin</span>
|
||||
</button>
|
||||
<button id="{{.ID}}menuTeamAdmin" class="flyout-item" style="display:none;">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
|
||||
<span>Team Admin</span>
|
||||
</button>
|
||||
<button id="{{.ID}}menuDebug" class="flyout-item" style="display:none;">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z"/><path d="M12 6V12L16 14"/></svg>
|
||||
<span>Debug Log</span>
|
||||
</button>
|
||||
<hr class="flyout-divider">
|
||||
<button id="{{.ID}}menuSignout" class="flyout-item flyout-danger">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
|
||||
<span>Sign Out</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -32,7 +32,7 @@
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
|
||||
Routing
|
||||
</a>
|
||||
<a href="{{$base}}/admin/settings" class="admin-cat-btn{{if eq $section "settings"}} active{{else if eq $section "storage"}} active{{else if eq $section "extensions"}} active{{end}}" data-cat="system">
|
||||
<a href="{{$base}}/admin/settings" class="admin-cat-btn{{if eq $section "settings"}} active{{else if eq $section "storage"}} active{{else if eq $section "extensions"}} active{{else if eq $section "channels"}} active{{else if eq $section "surfaces"}} active{{end}}" data-cat="system">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09"/></svg>
|
||||
System
|
||||
</a>
|
||||
@@ -241,4 +241,5 @@
|
||||
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/memory-ui.js?v={{.Version}}"></script>
|
||||
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/notifications.js?v={{.Version}}"></script>
|
||||
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/admin-scaffold.js?v={{.Version}}"></script>
|
||||
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/admin-surfaces.js?v={{.Version}}"></script>
|
||||
{{end}}
|
||||
|
||||
@@ -177,45 +177,20 @@ window.addEventListener('unhandledrejection', function(e) {
|
||||
|
||||
{{/* User / bottom */}}
|
||||
<div class="sidebar-bottom">
|
||||
{{if .SurfaceEnabled "notes"}}
|
||||
<button id="notesBtn" class="sb-btn sidebar-notes-btn">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
|
||||
<span class="sb-label">Notes</span>
|
||||
</button>
|
||||
{{/* Editor shortcut */}}
|
||||
{{end}}
|
||||
{{if .SurfaceEnabled "editor"}}
|
||||
<a href="{{.BasePath}}/editor" class="sb-btn sidebar-editor-btn" title="Editor">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>
|
||||
<span class="sb-label">Editor</span>
|
||||
</a>
|
||||
{{end}}
|
||||
<div class="sidebar-bottom-divider"></div>
|
||||
<button id="userMenuBtn" class="user-btn">
|
||||
<div id="userAvatar" class="user-avatar">
|
||||
<span id="avatarLetter">?</span>
|
||||
</div>
|
||||
<span id="userName" class="sb-label">User</span>
|
||||
</button>
|
||||
<div id="userFlyout" class="user-flyout">
|
||||
<button id="menuSettings" class="flyout-item">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.32 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
|
||||
<span>Settings</span>
|
||||
</button>
|
||||
<button id="menuAdmin" class="flyout-item" style="display:none;">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
|
||||
<span>Admin</span>
|
||||
</button>
|
||||
<button id="menuTeamAdmin" class="flyout-item" style="display:none;">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
|
||||
<span>Team Admin</span>
|
||||
</button>
|
||||
<button id="menuDebug" class="flyout-item" style="display:none;">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z"/><path d="M12 6V12L16 14"/></svg>
|
||||
<span>Debug Log</span>
|
||||
</button>
|
||||
<hr class="flyout-divider">
|
||||
<button id="menuSignout" class="flyout-item flyout-danger">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
|
||||
<span>Sign Out</span>
|
||||
</button>
|
||||
</div>
|
||||
{{template "user-menu" dict "ID" ""}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -233,16 +208,7 @@ window.addEventListener('unhandledrejection', function(e) {
|
||||
|
||||
{{/* Chat header: model selector + caps + tokens + panel + notif */}}
|
||||
<div class="chat-header">
|
||||
<div id="modelDropdown" class="model-dropdown">
|
||||
<button id="modelDropdownBtn" class="model-dropdown-btn">
|
||||
<span id="modelDropdownLabel" class="model-dropdown-label">Select model…</span>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
|
||||
</button>
|
||||
<div id="modelDropdownMenu" class="model-dropdown-menu">
|
||||
{{/* Populated by UI.updateModelSelector() */}}
|
||||
</div>
|
||||
</div>
|
||||
<span id="modelCaps" class="model-caps"></span>
|
||||
{{template "model-selector" dict "ID" ""}}
|
||||
<div class="chat-header-right">
|
||||
<span id="chatTokenCount" class="token-count" title="Estimated token count"></span>
|
||||
<span id="summarizedHistory" class="summarized-badge" style="display:none;" title="Earlier messages are summarized">📋 Summarized</span>
|
||||
|
||||
@@ -1,127 +1,104 @@
|
||||
{{/*
|
||||
Editor surface — matches switchboard-prototype-editor.jsx.
|
||||
IDE-like layout: topbar + file tree + tabs + code + chat pane.
|
||||
editor-mode.js builds CodeMirror, file tree, etc.
|
||||
Editor surface — v0.25.0 rebuild.
|
||||
Three-pane layout: files | code-editor | <chat, notes>
|
||||
Uses PaneContainer for resizable splits and tabbed right pane.
|
||||
|
||||
Components are SERVER-RENDERED via Go template partials into hidden
|
||||
containers (#editorComponents). The boot script moves them into
|
||||
pane slots created by PaneContainer. This ensures full DOM parity
|
||||
with the chat surface — same buttons, same features, same behavior.
|
||||
*/}}
|
||||
|
||||
{{define "surface-editor"}}
|
||||
<div class="surface-editor">
|
||||
<div class="surface-editor" id="editorSurface">
|
||||
|
||||
{{/* Top Bar */}}
|
||||
<div class="editor-topbar">
|
||||
<a href="{{.BasePath}}/" class="editor-topbar-back">
|
||||
{{/* ── Top Bar ─────────────────────────── */}}
|
||||
<div class="editor-topbar" id="editorTopbar">
|
||||
<a href="{{.BasePath}}/" class="editor-topbar-back" title="Back to chat">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>
|
||||
Back
|
||||
</a>
|
||||
<div style="width:1px;height:18px;background:var(--border);"></div>
|
||||
<div class="editor-topbar-sep"></div>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color:var(--accent);"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>
|
||||
<span style="font-size:13px;font-weight:600;" id="editorWorkspaceName">
|
||||
{{if .Data}}{{.Data.WorkspaceName}}{{else}}Editor{{end}}
|
||||
</span>
|
||||
<div style="display:flex;align-items:center;gap:4px;background:var(--purple-dim);padding:2px 8px;border-radius:4px;">
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="var(--purple)" stroke-width="2"><circle cx="12" cy="18" r="3"/><circle cx="12" cy="6" r="3"/><line x1="12" y1="9" x2="12" y2="15"/></svg>
|
||||
<span style="font-size:11px;font-weight:600;color:var(--purple);font-family:var(--mono);">main</span>
|
||||
</div>
|
||||
<div style="flex:1;"></div>
|
||||
<button class="icon-btn" id="editorNewFileBtn" title="New File">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
||||
</button>
|
||||
<button class="icon-btn" id="editorRefreshBtn" title="Refresh">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
|
||||
</button>
|
||||
<button class="icon-btn" id="editorSearchBtn" title="Search">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
</button>
|
||||
<div style="width:1px;height:18px;background:var(--border);"></div>
|
||||
<button class="icon-btn" id="editorToggleTree" title="Toggle file tree">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="9" y1="3" x2="9" y2="21"/></svg>
|
||||
</button>
|
||||
<button class="icon-btn" id="editorToggleChat" title="Toggle chat">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="15" y1="3" x2="15" y2="21"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{/* Body: tree + editor + chat */}}
|
||||
<div class="editor-body" id="editorBody">
|
||||
{{/* File Tree */}}
|
||||
<div class="editor-tree" id="editorFileTree">
|
||||
<div class="editor-tree-header">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
|
||||
<span style="font-size:11px;font-weight:600;color:var(--text-2);text-transform:uppercase;letter-spacing:0.4px;flex:1;">Files</span>
|
||||
<button class="icon-btn" id="treeNewFileBtn" title="New file">
|
||||
{{/* Workspace selector dropdown */}}
|
||||
<div class="editor-ws-selector" id="editorWsSelector">
|
||||
<button class="editor-ws-selector-btn" id="editorWsSelectorBtn">
|
||||
<span id="editorWorkspaceName">{{if .Data}}{{.Data.WorkspaceName}}{{else}}Editor{{end}}</span>
|
||||
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
|
||||
</button>
|
||||
<div class="editor-ws-dropdown" id="editorWsDropdown">
|
||||
<div id="editorWsList" class="editor-ws-list">
|
||||
{{/* Populated by JS */}}
|
||||
</div>
|
||||
<div class="editor-ws-dropdown-divider"></div>
|
||||
<button class="editor-ws-dropdown-item editor-ws-new" id="editorWsNewBtn">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
||||
New Workspace
|
||||
</button>
|
||||
</div>
|
||||
<div class="editor-tree-files" id="editorTreeFiles">
|
||||
{{/* Populated by editor-mode.js */}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{/* Main Editor Area */}}
|
||||
<div class="editor-main" id="editorMain">
|
||||
{{/* Tabs */}}
|
||||
<div class="editor-tabs" id="editorTabs">
|
||||
{{/* Populated by editor-mode.js */}}
|
||||
</div>
|
||||
|
||||
{{/* Code Content */}}
|
||||
<div class="editor-content" id="editorContent">
|
||||
<div style="display:flex;align-items:center;justify-content:center;height:100%;color:var(--text-3);">
|
||||
<div style="text-align:center;">
|
||||
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" style="opacity:0.12;display:inline-block;margin-bottom:10px;"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><polyline points="13 2 13 9 20 9"/></svg>
|
||||
<p style="margin:0;font-size:14px;">Open a file to start editing</p>
|
||||
<p style="margin:6px 0 0;font-size:12px;">Select from the file tree or Ctrl+P</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{/* Status Bar */}}
|
||||
<div class="editor-statusbar" id="editorStatusbar">
|
||||
<span>Ready</span>
|
||||
</div>
|
||||
<div class="editor-topbar-branch" id="editorBranchBadge" style="display:none;">
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="var(--purple)" stroke-width="2"><circle cx="12" cy="18" r="3"/><circle cx="12" cy="6" r="3"/><line x1="12" y1="9" x2="12" y2="15"/></svg>
|
||||
<span id="editorBranchName" class="editor-topbar-branch-text">main</span>
|
||||
</div>
|
||||
<div style="flex:1;"></div>
|
||||
<button class="icon-btn" id="editorRefreshBtn" title="Refresh files">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
|
||||
</button>
|
||||
{{template "user-menu" dict "ID" ""}}
|
||||
</div>
|
||||
|
||||
{{/* Chat Pane (resizable) */}}
|
||||
<div id="editorSplitHandle" style="width:5px;background:var(--border);cursor:col-resize;flex-shrink:0;display:none;"></div>
|
||||
<div class="editor-chat" id="editorChatPane" style="display:none;">
|
||||
<div class="editor-chat-header">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
|
||||
<span style="font-size:12px;font-weight:600;flex:1;">AI Chat</span>
|
||||
<span class="badge badge-accent" id="editorChatModel">claude-sonnet-4-5</span>
|
||||
{{/* ── Pane Body ───────────────────────── */}}
|
||||
<div class="editor-body" id="editorBody"></div>
|
||||
|
||||
{{/* ── Bootstrap (no workspace) ────────── */}}
|
||||
<div class="editor-bootstrap" id="editorBootstrap" style="display:none;">
|
||||
<div class="editor-bootstrap-card">
|
||||
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" stroke-width="1.5" style="opacity:0.6;margin-bottom:12px;">
|
||||
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
|
||||
</svg>
|
||||
<h3 style="margin:0 0 16px;font-size:16px;">Open a Workspace</h3>
|
||||
<div id="editorBootstrapList" style="margin-bottom:16px;">
|
||||
<div style="font-size:12px;color:var(--text-3);">Loading workspaces…</div>
|
||||
</div>
|
||||
<div class="editor-chat-messages" id="editorChatMessages">
|
||||
{{/* Populated by editor-mode.js */}}
|
||||
</div>
|
||||
<div class="editor-chat-input">
|
||||
<div style="display:flex;gap:8px;align-items:flex-end;">
|
||||
<textarea id="editorChatInput" placeholder="Ask about this code…" rows="2"
|
||||
style="flex:1;background:var(--input-bg,var(--bg));border:1px solid var(--border);color:var(--text);padding:8px 10px;border-radius:8px;font-size:12px;font-family:inherit;outline:none;resize:none;"></textarea>
|
||||
<button id="editorChatSendBtn" style="background:var(--accent);color:#fff;border:none;border-radius:8px;padding:8px 14px;font-size:12px;font-weight:600;cursor:pointer;font-family:inherit;">Send</button>
|
||||
</div>
|
||||
<div style="display:flex;gap:6px;margin-top:6px;font-size:10px;color:var(--text-3);">
|
||||
<span id="editorChatContext">Context: none</span>
|
||||
<span>·</span>
|
||||
<span>Cmd+L to focus</span>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;">
|
||||
<div style="flex:1;height:1px;background:var(--border);"></div>
|
||||
<span style="font-size:11px;color:var(--text-3);text-transform:uppercase;">or create new</span>
|
||||
<div style="flex:1;height:1px;background:var(--border);"></div>
|
||||
</div>
|
||||
<input type="text" id="editorBootstrapName" class="editor-bootstrap-input" placeholder="Workspace name" value="workspace">
|
||||
<button id="editorBootstrapBtn" class="editor-bootstrap-btn">Create Workspace</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{/* ── Server-Rendered Components ──────── */}}
|
||||
{{/* Hidden until moved into pane slots by editor-surface.js.
|
||||
Full DOM from Go template partials = feature parity with chat surface. */}}
|
||||
<div id="editorComponents" style="display:none;">
|
||||
{{template "file-tree" dict "ID" "ed"}}
|
||||
{{template "code-editor" dict "ID" "ed"}}
|
||||
{{template "chat-pane" dict "ID" "ed"}}
|
||||
{{template "note-editor" dict "ID" "edNotes"}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="toastContainer" class="toast-container"></div>
|
||||
{{end}}
|
||||
|
||||
{{/* CSS: loaded via base.html (variables, layout, primitives, modals, chat, panels, surfaces, splash) */}}
|
||||
{{/* ── CSS ─────────────────────────────────── */}}
|
||||
{{define "css-editor"}}
|
||||
<link rel="stylesheet" href="{{.BasePath}}/css/editor-mode.css?v={{.Version}}">
|
||||
<link rel="stylesheet" href="{{.BasePath}}/css/editor-surface.css?v={{.Version}}">
|
||||
{{end}}
|
||||
|
||||
{{/* Scripts */}}
|
||||
{{/* ── Scripts ─────────────────────────────── */}}
|
||||
{{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}}/js/ui-format.js?v={{.Version}}"></script>
|
||||
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/editor-mode.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/app.js?v={{.Version}}"></script>
|
||||
{{end}}
|
||||
|
||||
Reference in New Issue
Block a user