fix: side peek des bases repasse en vanilla JS + largeur 1100px standard (v7.49.0)
- Panneau peek: les bindings Alpine (x-data absent du conteneur) rendaient loovverture et le redimensionnement inoperants -> cblage direct sur le document. - Helper unique window.fdWirePeekResize (app.js): pointer capture, 300px-90vw, clic=fermer, largeur persiste fd_peek_width partagee entre les 4 peeks. - database-table-container margin:0 (tableau colle a gauche, marge Library). - .lib-container remonte dans app.css (trash etait pleine largeur), .db-index 1100px. - ObsiGate verifie sans code: creation .xlsx OK (openpyxl, #186).
This commit is contained in:
@@ -125,13 +125,21 @@
|
||||
<div class="db-col-menu-item" id="db-col-insert-right"><span>→┤ Insert right</span></div>
|
||||
</div>
|
||||
|
||||
<!-- Side Peek Panel for Database Pages -->
|
||||
<div id="db-side-peek" class="db-side-peek" style="display:none">
|
||||
<div class="db-side-peek-resize" id="db-side-peek-resize"></div>
|
||||
<div class="db-side-peek-header">
|
||||
<button id="db-side-peek-close" class="db-side-peek-close">×</button>
|
||||
<button id="db-side-peek-full" class="db-side-peek-full" title="Open full page">↗</button>
|
||||
</div>
|
||||
<iframe id="db-side-peek-iframe" src="" style="width:100%;height:100%;border:none"></iframe>
|
||||
<!-- Side Peek Panel for Database Pages (vanilla JS, piloté par database_table.js) -->
|
||||
<div id="db-side-peek" class="db-side-peek">
|
||||
<div class="peek-resize-handle" title="Glisser pour redimensionner — cliquer pour fermer"></div>
|
||||
<div class="db-side-peek-header">
|
||||
<button id="db-side-peek-close" title="Close">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||||
</button>
|
||||
<div class="db-side-peek-title" id="db-side-peek-title"></div>
|
||||
<button id="db-side-peek-full" title="Open full page">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="db-side-peek-body">
|
||||
<div class="db-side-peek-loading" id="db-side-peek-loading">Chargement…</div>
|
||||
<iframe id="db-side-peek-iframe" src="about:blank" title="Document" style="width:100%;height:100%;border:none;flex:1;min-height:0;"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -107,4 +107,6 @@
|
||||
.db-list-cell{color:var(--text-secondary);font-size:12px;min-width:110px}
|
||||
</style>
|
||||
<script type="application/json" id="db-config" nonce="{{ csp_nonce() }}">{{ ({"collection": collection_data.collection, "properties": collection_data.properties, "pages": collection_data.pages, "views": collection_data.views if collection_data.views is defined else []} if collection_data else none) | tojson }}</script>
|
||||
<link rel="stylesheet" href="/static/css/my_tasks.css?v={{ asset_version }}">
|
||||
<script data-cfasync="false" src="/static/js/database_table.js?v={{ asset_version }}"></script>
|
||||
<script data-cfasync="false" src="/static/js/task_db_link.js?v={{ asset_version }}" defer></script>
|
||||
|
||||
@@ -2,12 +2,19 @@
|
||||
<style>
|
||||
#fd-agent-fab{position:fixed;right:22px;bottom:22px;width:54px;height:54px;border-radius:50%;background:var(--text-primary,#37352F);color:var(--bg-primary,#fff);border:none;cursor:pointer;box-shadow:0 8px 26px rgba(0,0,0,.35);display:flex;align-items:center;justify-content:center;z-index:1700;transition:transform .15s,box-shadow .15s}
|
||||
#fd-agent-fab:hover{transform:scale(1.06);box-shadow:0 12px 34px rgba(0,0,0,.45)}
|
||||
#fd-agent-panel{position:fixed;top:0;right:0;bottom:0;width:470px;max-width:100vw;background:var(--bg-primary,#191919);border-left:1px solid var(--border,rgba(255,255,255,.1));z-index:1850;display:flex;flex-direction:column;box-shadow:-16px 0 48px rgba(0,0,0,.55);transform:translateX(102%);transition:transform .24s cubic-bezier(.16,1,.3,1)}
|
||||
#fd-agent-panel{position:fixed;top:0;right:0;bottom:0;width:var(--fd-agent-width,470px);max-width:100vw;background:var(--bg-primary,#191919);border-left:1px solid var(--border,rgba(255,255,255,.1));z-index:1850;display:flex;flex-direction:column;box-shadow:-16px 0 48px rgba(0,0,0,.55);transform:translateX(102%);transition:transform .24s cubic-bezier(.16,1,.3,1)}
|
||||
#fd-agent-panel.open{transform:translateX(0)}
|
||||
#fd-agent-panel ::-webkit-scrollbar{width:8px;height:8px}
|
||||
#fd-agent-panel ::-webkit-scrollbar-thumb{background:rgba(128,128,128,.35);border-radius:4px}
|
||||
#fd-agent-panel ::-webkit-scrollbar-track{background:transparent}
|
||||
|
||||
/* Resize handle (left edge) */
|
||||
.fd-ap-resize{position:absolute;top:0;bottom:0;left:0;width:6px;cursor:col-resize;z-index:5}
|
||||
.fd-ap-resize::before{content:"";position:absolute;top:0;bottom:0;left:2px;width:2px;background:transparent;border-radius:2px;transition:background .12s ease}
|
||||
.fd-ap-resize:hover::before,body.fd-ap-resizing .fd-ap-resize::before{background:var(--accent,#2383E2)}
|
||||
body.fd-ap-resizing{cursor:col-resize!important;user-select:none}
|
||||
body.fd-ap-resizing *{cursor:col-resize!important}
|
||||
|
||||
/* Header */
|
||||
.fd-ap-head{display:flex;align-items:center;gap:12px;padding:16px 18px 12px}
|
||||
.fd-ap-logo{width:36px;height:36px;border-radius:50%;background:var(--text-primary,#37352F);color:var(--bg-primary,#fff);display:flex;align-items:center;justify-content:center;flex-shrink:0;box-shadow:0 2px 8px rgba(0,0,0,.25)}
|
||||
@@ -189,7 +196,7 @@
|
||||
.fd-agent-msg-markdown ul,.fd-agent-msg-markdown ol{margin:0 0 8px;padding-left:20px}
|
||||
.fd-agent-msg-markdown li{margin:2px 0}
|
||||
.fd-agent-msg-markdown code{background:rgba(127,127,127,.18);border-radius:4px;padding:1px 5px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.9em}
|
||||
.fd-agent-msg-markdown pre{background:var(--bg-code,#111);border:1px solid var(--border,#333);border-radius:8px;padding:12px;overflow-x:auto;margin:8px 0}
|
||||
.fd-agent-msg-markdown pre{background:var(--bg-code,var(--bg-tertiary,#1f1f1f));border:1px solid var(--border,rgba(255,255,255,.12));border-radius:8px;padding:12px;overflow-x:auto;margin:8px 0;color:var(--text,#ececec)}
|
||||
.fd-agent-msg-markdown pre code{background:none;padding:0;font-size:12px;line-height:1.5}
|
||||
.fd-md-table-wrap{overflow-x:auto;margin:8px 0}
|
||||
.fd-md-table{border-collapse:collapse;width:100%;font-size:13px}
|
||||
@@ -211,6 +218,7 @@
|
||||
|
||||
{# ── Panel ── #}
|
||||
<div id="fd-agent-panel" class="fd-ap" :class="{open: open}" x-data="agentPanel()" @keydown.escape.window="close()" role="dialog" aria-label="Agent FlowDeck">
|
||||
<div class="fd-ap-resize" x-ref="resizeHandle" role="separator" aria-orientation="vertical" aria-label="Redimensionner le panneau" tabindex="0" title="Glisser pour redimensionner — double-clic pour réinitialiser"></div>
|
||||
<div class="fd-ap-head">
|
||||
<span class="fd-ap-logo"><svg viewBox="0 0 24 24" width="19" height="19" fill="currentColor" aria-hidden="true"><path d="M12 3l1.9 5.8a2 2 0 0 0 1.3 1.3L21 12l-5.8 1.9a2 2 0 0 0-1.3 1.3L12 21l-1.9-5.8a2 2 0 0 0-1.3-1.3L3 12l5.8-1.9a2 2 0 0 0 1.3-1.3L12 3Z"/></svg></span>
|
||||
<div class="fd-ap-head-main">
|
||||
|
||||
+324
-7
@@ -13,6 +13,11 @@
|
||||
<link rel="stylesheet" href="/static/css/app.css?v={{ asset_version }}">
|
||||
<link rel="stylesheet" href="/static/css/design-tokens.css?v={{ asset_version }}">
|
||||
<link rel="stylesheet" href="/static/css/components.css?v={{ asset_version }}">
|
||||
<!-- My Tasks + page « /db » : lies dans le SHELL et non dans la zone
|
||||
swappée. Un <link> à l'intérieur de `.main-wrapper` peut être retiré par
|
||||
htmx lors d'une navigation partielle : la page revenait alors sans
|
||||
aucune feuille de style (composants blancs en thème sombre). -->
|
||||
<link rel="stylesheet" href="/static/css/my_tasks.css?v={{ asset_version }}">
|
||||
<link rel="stylesheet" href="/static/css/katex.min.css?v=0.16.11">
|
||||
<style>
|
||||
/* ── Mobile responsive (v4.0.2) ── */
|
||||
@@ -241,24 +246,38 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Navigation icons row -->
|
||||
<div class="sidebar-nav-row">
|
||||
<a href="{% if local_workspaces and local_workspaces|length > 0 %}/local-workspace?ws={{ local_workspaces[0].id }}{% else %}/workspaces{% endif %}" class="nav-icon-btn home-btn" :class="{ active: currentView === 'home' }" title="Home">
|
||||
<!-- Navigation tabs row — collapsible pills: the active tab expands with
|
||||
its label, the others collapse to their icon. Selecting a tab replaces
|
||||
the whole sidebar content (Home tree / Chat history / Meetings / Inbox). -->
|
||||
<div class="sidebar-nav-row" role="tablist" aria-label="Sidebar sections">
|
||||
<a href="{% if local_ws_id %}/local-workspace?ws={{ local_ws_id }}{% elif local_workspaces and local_workspaces|length > 0 %}/local-workspace?ws={{ local_workspaces[0].id }}{% else %}/workspaces{% endif %}" class="nav-icon-btn home-btn nav-tab-btn" data-tab="home" :class="{ active: sidebarTab === 'home' }" @click="setSidebarTab('home')" :aria-selected="sidebarTab === 'home'" role="tab" title="Home">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z"/></svg>
|
||||
Home
|
||||
<span class="nav-tab-label" :style="sidebarTab === 'home' ? 'display:inline-block' : 'display:none'">Home</span>
|
||||
</a>
|
||||
<button class="nav-icon-btn" title="Chat (Ctrl+J)" onclick="window.fdAgent&&window.fdAgent.open()">
|
||||
<button type="button" class="nav-icon-btn nav-tab-btn" data-tab="chat" :class="{ active: sidebarTab === 'chat' }" @click="setSidebarTab('chat')" :aria-selected="sidebarTab === 'chat'" role="tab" title="Chat (Ctrl+J)">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/></svg>
|
||||
<span class="nav-tab-label" :style="sidebarTab === 'chat' ? 'display:inline-block' : 'display:none'">Chat</span>
|
||||
<span class="nav-tab-badge" x-show="chatHistory.length" x-cloak x-text="chatHistory.length" :style="sidebarTab === 'chat' ? 'display:none' : ''"></span>
|
||||
</button>
|
||||
<button class="nav-icon-btn" title="Inbox">
|
||||
<button type="button" class="nav-icon-btn nav-tab-btn" data-tab="meetings" :class="{ active: sidebarTab === 'meetings' }" @click="setSidebarTab('meetings')" :aria-selected="sidebarTab === 'meetings'" role="tab" title="Meetings">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
|
||||
<span class="nav-tab-label" :style="sidebarTab === 'meetings' ? 'display:inline-block' : 'display:none'">Meeting</span>
|
||||
<span class="nav-tab-badge" x-show="meetingToday.length" x-cloak x-text="meetingToday.length" :style="sidebarTab === 'meetings' ? 'display:none' : ''"></span>
|
||||
</button>
|
||||
<button type="button" class="nav-icon-btn nav-tab-btn" data-tab="inbox" :class="{ active: sidebarTab === 'inbox' }" @click="setSidebarTab('inbox')" :aria-selected="sidebarTab === 'inbox'" role="tab" title="Inbox">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="22 12 16 12 14 15 10 15 8 12 2 12"/><path d="M5.45 5.11L2 12v6a2 2 0 002 2h16a2 2 0 002-2v-6l-3.45-6.89A2 2 0 0016.76 4H7.24a2 2 0 00-1.79 1.11z"/></svg>
|
||||
<span class="nav-tab-label" :style="sidebarTab === 'inbox' ? 'display:inline-block' : 'display:none'">Inbox</span>
|
||||
<span class="nav-tab-badge nav-tab-badge-accent" x-show="inboxUnread > 0" x-cloak x-text="inboxUnread > 99 ? '99+' : inboxUnread" :style="sidebarTab === 'inbox' ? 'display:none' : ''"></span>
|
||||
</button>
|
||||
<button class="nav-icon-btn" @click="openQuickFind" title="Search" style="margin-left:auto;">
|
||||
<button type="button" class="nav-icon-btn" @click="openQuickFind" title="Search" style="margin-left:auto;">
|
||||
<svg width="18" height="18" 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>
|
||||
|
||||
<div class="sidebar-scroll">
|
||||
|
||||
<!-- ── TAB: Home (workspace tree, recents, favorites, libraries…) ── -->
|
||||
<div class="sidebar-tab-panel" x-show="sidebarTab === 'home'" x-cloak>
|
||||
<!-- Workspace / Private -->
|
||||
<div class="sidebar-section" x-show="isSectionVisible('workspace')" x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100">
|
||||
<div class="sidebar-section-header" @click="toggleSection('workspace')" @contextmenu.prevent="openSectionMenu($event, 'workspace')">
|
||||
@@ -571,6 +590,123 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div><!-- /sidebar-tab-panel home -->
|
||||
|
||||
<!-- ── TAB: Chat (agent shortcuts + conversation history) ── -->
|
||||
<div class="sidebar-tab-panel" x-show="sidebarTab === 'chat'" x-cloak>
|
||||
<div class="sb-panel-header">
|
||||
<span class="sb-panel-title">Chat</span>
|
||||
<button type="button" class="sb-panel-action" @click="chatNewConversation()" title="New conversation">{{ fd_icon("plus",14) }}</button>
|
||||
</div>
|
||||
|
||||
<!-- Circular agent shortcuts -->
|
||||
<div class="sb-agent-row" x-show="agentList.length">
|
||||
<template x-for="a in agentList.slice(0, 8)" :key="a.id">
|
||||
<button type="button" class="sb-agent-avatar" :title="a.name" @click="agentOpen(a.id)">
|
||||
<span x-text="a.icon || '\u{1F916}'"></span>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-subgroup">
|
||||
<div class="sidebar-subgroup-label">Past 30 days</div>
|
||||
<ul class="sidebar-items">
|
||||
<template x-for="c in chatRecent" :key="c.id">
|
||||
<li class="sidebar-item" @click="chatOpenConversation(c.id)">
|
||||
<span class="page-icon page-icon-svg">{{ fd_icon("message-square",14) }}</span>
|
||||
<span class="page-name" x-text="c.title || 'New conversation'"></span>
|
||||
</li>
|
||||
</template>
|
||||
<li class="sidebar-item empty-hint" x-show="!chatLoading && !chatRecent.length">
|
||||
<span class="page-name text-dim">No recent conversations</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-subgroup" x-show="chatOlder.length">
|
||||
<div class="sidebar-subgroup-label">Older</div>
|
||||
<ul class="sidebar-items">
|
||||
<template x-for="c in chatOlder" :key="c.id">
|
||||
<li class="sidebar-item" @click="chatOpenConversation(c.id)">
|
||||
<span class="page-icon page-icon-svg">{{ fd_icon("message-square",14) }}</span>
|
||||
<span class="page-name" x-text="c.title || 'New conversation'"></span>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-item empty-hint" x-show="chatLoading" style="padding:8px;">
|
||||
<span class="page-name text-dim">Loading…</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── TAB: Meeting (upcoming events + quick actions) ── -->
|
||||
<div class="sidebar-tab-panel" x-show="sidebarTab === 'meetings'" x-cloak>
|
||||
<div class="sb-panel-header">
|
||||
<span class="sb-panel-title">Meeting</span>
|
||||
<button type="button" class="sb-panel-action" @click="loadMeetings(true)" title="Refresh">{{ fd_icon("refresh",14) }}</button>
|
||||
</div>
|
||||
|
||||
<div class="sb-filter-row">
|
||||
<button type="button" class="sb-filter-chip" :class="{ active: meetingFilter === 'upcoming' }" @click="meetingFilter = 'upcoming'">Upcoming</button>
|
||||
<button type="button" class="sb-filter-chip" :class="{ active: meetingFilter === 'today' }" @click="meetingFilter = 'today'">Today</button>
|
||||
</div>
|
||||
|
||||
<div class="sb-quick-actions">
|
||||
<button type="button" class="sb-quick-btn" @click="meetingTranscribe()">{{ fd_icon("upload",14) }} Transcribe a podcast</button>
|
||||
<button type="button" class="sb-quick-btn" @click="meetingNote()">{{ fd_icon("sparkles",14) }} New AI meeting note</button>
|
||||
<input type="file" x-ref="meetingAudio" accept="audio/*" class="sb-hidden-input" @change="meetingUpload($event)">
|
||||
</div>
|
||||
|
||||
<ul class="sidebar-items">
|
||||
<template x-for="ev in meetingVisible" :key="ev.collection_id + '-' + ev.id">
|
||||
<li class="sidebar-item" @click="meetingOpen(ev)">
|
||||
<span class="page-icon page-icon-svg">{{ fd_icon("calendar",14) }}</span>
|
||||
<span class="page-name" x-text="ev.title"></span>
|
||||
<span class="sb-item-meta" x-text="ev.today ? 'Today' : ev.date"></span>
|
||||
</li>
|
||||
</template>
|
||||
<li class="sidebar-item empty-hint" x-show="!meetingsLoading && !meetingVisible.length">
|
||||
<span class="page-name text-dim" x-text="meetingFilter === 'today' ? 'Nothing scheduled today' : 'No upcoming meetings'"></span>
|
||||
</li>
|
||||
<li class="sidebar-item empty-hint" x-show="meetingsLoading">
|
||||
<span class="page-name text-dim">Loading…</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- ── TAB: Inbox (notifications) ── -->
|
||||
<div class="sidebar-tab-panel" x-show="sidebarTab === 'inbox'" x-cloak>
|
||||
<div class="sb-panel-header">
|
||||
<span class="sb-panel-title">Inbox <span class="sb-panel-count" x-show="inboxUnread > 0" x-text="inboxUnread"></span></span>
|
||||
<div style="display:flex;gap:4px;">
|
||||
<button type="button" class="sb-panel-action" x-show="inboxUnread > 0" @click="inboxMarkAllRead()" title="Mark all read">{{ fd_icon("check-square",14) }}</button>
|
||||
<button type="button" class="sb-panel-action" @click="inboxFilter = (inboxFilter === 'all' ? 'unread' : 'all')"
|
||||
:title="inboxFilter === 'all' ? 'Show unread only' : 'Show all'" x-text="inboxFilter === 'all' ? 'All' : 'Unread'"></button>
|
||||
<button type="button" class="sb-panel-action" @click="loadInbox()" title="Refresh">{{ fd_icon("refresh",14) }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="sidebar-items">
|
||||
<template x-for="n in inboxVisible" :key="n.id">
|
||||
<li class="sidebar-item" :class="{ 'sb-item-unread': !n.is_read }" @click="inboxOpen(n)">
|
||||
<span class="page-icon page-icon-svg">{{ fd_icon("bell",14) }}</span>
|
||||
<span style="flex:1;min-width:0;">
|
||||
<span class="page-name" style="display:block;" x-text="n.title"></span>
|
||||
<span class="sb-item-sub" x-text="n.message"></span>
|
||||
</span>
|
||||
</li>
|
||||
</template>
|
||||
<li class="sidebar-item empty-hint" x-show="!inboxLoading && !inboxVisible.length" style="flex-direction:column;align-items:center;gap:4px;padding:22px 8px;">
|
||||
<span style="font-size:13px;font-weight:600;color:var(--text-secondary);">Inbox Zero</span>
|
||||
<span class="page-name text-dim">You’re all caught up</span>
|
||||
</li>
|
||||
<li class="sidebar-item empty-hint" x-show="inboxLoading">
|
||||
<span class="page-name text-dim">Loading…</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="sidebar-footer">
|
||||
<div style="display:flex; gap:6px;">
|
||||
@@ -1128,6 +1264,15 @@
|
||||
Alpine.data('appState', appState);
|
||||
});
|
||||
|
||||
/** Parse a SQL/ISO timestamp ("2026-10-04 12:00:00") into a ms epoch.
|
||||
Shared by the sidebar Chat tab (grouping by recency). */
|
||||
window.fdTime = function(value) {
|
||||
if (!value) return 0;
|
||||
var t = new Date(String(value).replace(' ', 'T'));
|
||||
if (isNaN(t.getTime())) t = new Date(value);
|
||||
return isNaN(t.getTime()) ? 0 : t.getTime();
|
||||
};
|
||||
|
||||
function appState() {
|
||||
return {
|
||||
init() {
|
||||
@@ -1135,6 +1280,8 @@
|
||||
this.loadSidebarConfig();
|
||||
this.loadAgents();
|
||||
this.loadTeamspaces();
|
||||
// Badge counts on the collapsed tabs (lazy fetch, no blocking).
|
||||
this.loadInbox();
|
||||
this.initSidebarWidth();
|
||||
document.addEventListener('fd-toggle-sidebar', () => this.toggleSidebar());
|
||||
this.startClipAutoRefresh();
|
||||
@@ -1240,6 +1387,176 @@
|
||||
giteaRepo: '{{ gitea_repo|default("") }}',
|
||||
giteaTree: [], // loaded client-side for sidebar
|
||||
currentView: 'home',
|
||||
|
||||
// ── Collapsible sidebar tabs (Home / Chat / Meeting / Inbox) ──
|
||||
// The active tab renders as a pill with its label, the others collapse
|
||||
// to an icon; selecting one replaces the whole sidebar content.
|
||||
sidebarTab: (function() {
|
||||
try {
|
||||
var saved = localStorage.getItem('fd_sidebar_tab');
|
||||
return ['home', 'chat', 'meetings', 'inbox'].indexOf(saved) >= 0 ? saved : 'home';
|
||||
} catch (e) { return 'home'; }
|
||||
})(),
|
||||
setSidebarTab(tab) {
|
||||
this.sidebarTab = tab;
|
||||
this.currentView = tab;
|
||||
try { localStorage.setItem('fd_sidebar_tab', tab); } catch (e) {}
|
||||
if (tab === 'chat') this.loadChatHistory();
|
||||
else if (tab === 'meetings') this.loadMeetings();
|
||||
else if (tab === 'inbox') this.loadInbox();
|
||||
},
|
||||
|
||||
// ── Chat tab ──
|
||||
chatHistory: [],
|
||||
chatLoading: false,
|
||||
get chatRecent() {
|
||||
var cut = Date.now() - 30 * 86400000;
|
||||
return this.chatHistory.filter(function(c) { return fdTime(c.updated_at) >= cut; });
|
||||
},
|
||||
get chatOlder() {
|
||||
var cut = Date.now() - 30 * 86400000;
|
||||
return this.chatHistory.filter(function(c) { return fdTime(c.updated_at) < cut; });
|
||||
},
|
||||
loadChatHistory() {
|
||||
var self = this;
|
||||
this.chatLoading = true;
|
||||
return fetch('/api/agent/conversations', { credentials: 'same-origin' })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
self.chatHistory = (d && d.conversations) || [];
|
||||
self.chatLoading = false;
|
||||
})
|
||||
.catch(function() { self.chatLoading = false; });
|
||||
},
|
||||
chatOpenConversation(id) {
|
||||
if (window.fdAgent && window.fdAgent.openConversation) window.fdAgent.openConversation(id);
|
||||
else this.agentOpen();
|
||||
},
|
||||
chatNewConversation() {
|
||||
if (window.fdAgent && window.fdAgent.open) window.fdAgent.open();
|
||||
else this.agentNew();
|
||||
},
|
||||
|
||||
// ── Meeting tab ──
|
||||
meetings: [],
|
||||
meetingsLoading: false,
|
||||
meetingFilter: 'upcoming',
|
||||
get meetingVisible() {
|
||||
if (this.meetingFilter === 'today') {
|
||||
return this.meetings.filter(function(e) { return e.today; });
|
||||
}
|
||||
return this.meetings.filter(function(e) { return !e.today; });
|
||||
},
|
||||
get meetingToday() { return this.meetings.filter(function(e) { return e.today; }); },
|
||||
loadMeetings(force) {
|
||||
var self = this;
|
||||
if (!force && this.meetings.length) return Promise.resolve();
|
||||
this.meetingsLoading = true;
|
||||
return fetch('/api/v2/meetings/upcoming?days=30', { credentials: 'same-origin' })
|
||||
.then(function(r) { return r.ok ? r.json() : { events: [] }; })
|
||||
.then(function(d) {
|
||||
self.meetings = (d && d.events) || [];
|
||||
self.meetingsLoading = false;
|
||||
})
|
||||
.catch(function() { self.meetings = []; self.meetingsLoading = false; });
|
||||
},
|
||||
meetingOpen(ev) {
|
||||
var target = ev.url || ('/db/' + ev.collection_id);
|
||||
if (window.fdNavigate) window.fdNavigate(target);
|
||||
else window.location.href = target;
|
||||
},
|
||||
meetingTranscribe() {
|
||||
var input = this.$refs && this.$refs.meetingAudio;
|
||||
if (input) input.click();
|
||||
},
|
||||
meetingNote() {
|
||||
if (window.fdAgent && window.fdAgent.ask) {
|
||||
window.fdAgent.ask('/meeting-note');
|
||||
} else if (window.fdAgent && window.fdAgent.open) {
|
||||
window.fdAgent.open();
|
||||
}
|
||||
},
|
||||
meetingUpload(ev) {
|
||||
var self = this;
|
||||
var input = ev.target;
|
||||
var file = input && input.files && input.files[0];
|
||||
if (!file) return;
|
||||
var title = 'Podcast ' + new Date().toISOString().slice(0, 10);
|
||||
// A transcript row is attached to a page: create the meeting page first,
|
||||
// then upload the audio against it.
|
||||
fetch('/api/local-workspace/items', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': this.getCsrfToken() },
|
||||
body: JSON.stringify({ name: title, type: 'page' })
|
||||
})
|
||||
.then(function(r) { if (!r.ok) throw new Error('page'); return r.json(); })
|
||||
.then(function(page) {
|
||||
var fd = new FormData();
|
||||
fd.append('audio', file);
|
||||
fd.append('page_id', String(page.id));
|
||||
return fetch('/api/v2/meetings/transcribe', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'X-CSRF-Token': self.getCsrfToken() },
|
||||
body: fd
|
||||
}).then(function(r) {
|
||||
if (!r.ok) throw new Error('transcribe');
|
||||
return r.json();
|
||||
}).then(function() { return page; });
|
||||
})
|
||||
.then(function(page) {
|
||||
self.toast('Audio uploaded — transcription attached to "' + title + '"', 'success');
|
||||
window.location.href = '/pages/' + page.id;
|
||||
})
|
||||
.catch(function() { self.toast('Upload failed', 'error'); })
|
||||
.then(function() { if (input) input.value = ''; });
|
||||
},
|
||||
|
||||
// ── Inbox tab ──
|
||||
inboxItems: [],
|
||||
inboxUnread: 0,
|
||||
inboxLoading: false,
|
||||
inboxFilter: 'all',
|
||||
get inboxVisible() {
|
||||
if (this.inboxFilter === 'unread') {
|
||||
return this.inboxItems.filter(function(n) { return !n.is_read; });
|
||||
}
|
||||
return this.inboxItems;
|
||||
},
|
||||
loadInbox() {
|
||||
var self = this;
|
||||
this.inboxLoading = true;
|
||||
return fetch('/api/notifications?limit=50', { credentials: 'same-origin' })
|
||||
.then(function(r) { return r.ok ? r.json() : { notifications: [], unread: 0 }; })
|
||||
.then(function(d) {
|
||||
self.inboxItems = (d && d.notifications) || [];
|
||||
self.inboxUnread = (d && d.unread) || 0;
|
||||
self.inboxLoading = false;
|
||||
})
|
||||
.catch(function() { self.inboxLoading = false; });
|
||||
},
|
||||
inboxOpen(n) {
|
||||
if (!n.is_read) {
|
||||
var self = this;
|
||||
fetch('/api/notifications/read', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': this.getCsrfToken() },
|
||||
body: JSON.stringify({ id: n.id })
|
||||
}).then(function() { return self.loadInbox(); });
|
||||
}
|
||||
if (n.url) window.location.href = n.url;
|
||||
},
|
||||
inboxMarkAllRead() {
|
||||
var self = this;
|
||||
fetch('/api/notifications/read', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': this.getCsrfToken() },
|
||||
body: JSON.stringify({})
|
||||
}).then(function() { return self.loadInbox(); });
|
||||
},
|
||||
contextMenu: { visible: false, x: 0, y: 0, pageId: null, pageName: '' },
|
||||
contextActions: {},
|
||||
showNewPageMenu: false,
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
<style>
|
||||
/* ── Library layout ── */
|
||||
.lib-container{max-width:1100px;margin:0 auto;padding:20px 24px;}
|
||||
/* .lib-container est défini dans app.css (largeur standard des pages) */
|
||||
.lib-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;}
|
||||
.lib-header h1{font-size:24px;font-weight:700;color:var(--text-primary);margin:0;}
|
||||
.lib-header-actions{display:flex;align-items:center;gap:8px;}
|
||||
@@ -408,7 +408,7 @@
|
||||
<div class="peek-overlay" :class="{ open: peekItem !== null }" @click.outside="closePeek()">
|
||||
<div id="lib-peek-resize-handle" style="position:absolute;left:-4px;top:0;width:8px;height:100%;cursor:col-resize;z-index:501;" title="Drag to resize — click to close"></div>
|
||||
<template x-if="peekItem">
|
||||
<div style="display:flex;flex-direction:column;height:100%;">
|
||||
<div style="flex-shrink:0;">
|
||||
<div class="peek-header">
|
||||
<button @click="closePeek()" title="Close">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||||
@@ -445,11 +445,17 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="peek-body" style="flex:1;display:flex;flex-direction:column;">
|
||||
<iframe id="lib-peek-iframe" :src="peekItem ? '/pages/' + peekItem.id + '?embed=1' : ''" style="width:100%;height:100%;border:none;flex:1;"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<!-- Le corps et l'iframe sont HORS du <template x-if> : ils existent donc en
|
||||
permanence, ce qui permet de piloter `src` en JS (le build CSP
|
||||
d'Alpine n'evalue pas le binding `:src` — l'iframe restait donc vide).
|
||||
Charger `/pages/{id}` sans `?embed=1` : c'est exactement l'editeur
|
||||
plein ecran, le peek n'est plus une variante allegee. -->
|
||||
<div class="peek-body" style="flex:1;display:flex;flex-direction:column;min-height:0;">
|
||||
<div class="peek-loading" id="lib-peek-loading">Chargement…</div>
|
||||
<iframe id="lib-peek-iframe" src="about:blank" title="Document" style="width:100%;height:100%;border:none;flex:1;min-height:0;"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /x-data -->
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div x-data="trashData()" style="padding: 0 24px; max-width: 800px;">
|
||||
<div x-data="trashData()" class="lib-container">
|
||||
<!-- Toolbar : recherche + tri + emplacement + purge -->
|
||||
<div style="display:flex; align-items:center; gap:8px; margin-bottom:12px;">
|
||||
<div style="position:relative; flex:1;">
|
||||
|
||||
Reference in New Issue
Block a user