mop-agent 0.1.13 → 0.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/apps/web/app/assistant/page.tsx +153 -47
- package/apps/web/app/globals.css +215 -28
- package/apps/web/components/AppShell.tsx +67 -46
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ through MOP-FLOW. It stores project memory, performs semantic recall and
|
|
|
5
5
|
consolidation, serves grounded chat, and can request approved actions from a
|
|
6
6
|
linked FLOW node.
|
|
7
7
|
|
|
8
|
-
> **Release status:** release candidate `mop-agent@0.1.
|
|
8
|
+
> **Release status:** release candidate `mop-agent@0.1.15` contains the corrected VPS
|
|
9
9
|
> installer, one-time Admin setup/login flow, and simplified shared application shell
|
|
10
10
|
> with centered page titles and ChatGPT-inspired navigation.
|
|
11
11
|
> The canonical installation command is exactly `npx mop-agent`.
|
|
@@ -5,6 +5,9 @@ import { useEffect, useRef, useState } from "react";
|
|
|
5
5
|
import { useMemoryCore } from "@/components/AppShell";
|
|
6
6
|
|
|
7
7
|
type Turn = { role: "user" | "assistant"; content: string };
|
|
8
|
+
type SavedChat = { id: string; title: string; turns: Turn[]; updatedAt: number };
|
|
9
|
+
|
|
10
|
+
const CHAT_HISTORY_KEY = "mop-agent-chat-history-v1";
|
|
8
11
|
|
|
9
12
|
export default function AssistantPage() {
|
|
10
13
|
const { projects } = useMemoryCore();
|
|
@@ -13,6 +16,10 @@ export default function AssistantPage() {
|
|
|
13
16
|
const [input, setInput] = useState("");
|
|
14
17
|
const [busy, setBusy] = useState(false);
|
|
15
18
|
const [providerUsed, setProviderUsed] = useState("");
|
|
19
|
+
const [history, setHistory] = useState<SavedChat[]>([]);
|
|
20
|
+
const [historyReady, setHistoryReady] = useState(false);
|
|
21
|
+
const [activeChatId, setActiveChatId] = useState("");
|
|
22
|
+
const [historyCollapsed, setHistoryCollapsed] = useState(false);
|
|
16
23
|
const endRef = useRef<HTMLDivElement>(null);
|
|
17
24
|
|
|
18
25
|
useEffect(() => {
|
|
@@ -21,10 +28,69 @@ export default function AssistantPage() {
|
|
|
21
28
|
}).catch(() => {});
|
|
22
29
|
}, []);
|
|
23
30
|
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
try {
|
|
33
|
+
const stored = JSON.parse(window.localStorage.getItem(CHAT_HISTORY_KEY) ?? "[]");
|
|
34
|
+
if (Array.isArray(stored)) setHistory(stored.slice(0, 30));
|
|
35
|
+
setHistoryCollapsed(window.localStorage.getItem("mop-agent-history-collapsed") === "1");
|
|
36
|
+
} catch {
|
|
37
|
+
window.localStorage.removeItem(CHAT_HISTORY_KEY);
|
|
38
|
+
} finally {
|
|
39
|
+
setHistoryReady(true);
|
|
40
|
+
}
|
|
41
|
+
}, []);
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (!historyReady) return;
|
|
45
|
+
window.localStorage.setItem(CHAT_HISTORY_KEY, JSON.stringify(history.slice(0, 30)));
|
|
46
|
+
}, [history, historyReady]);
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (!activeChatId || turns.length === 0) return;
|
|
50
|
+
const firstMessage = turns.find((turn) => turn.role === "user")?.content ?? "New conversation";
|
|
51
|
+
setHistory((current) => {
|
|
52
|
+
const updated: SavedChat = {
|
|
53
|
+
id: activeChatId,
|
|
54
|
+
title: firstMessage.slice(0, 54),
|
|
55
|
+
turns,
|
|
56
|
+
updatedAt: Date.now(),
|
|
57
|
+
};
|
|
58
|
+
return [updated, ...current.filter((chat) => chat.id !== activeChatId)].slice(0, 30);
|
|
59
|
+
});
|
|
60
|
+
}, [activeChatId, turns]);
|
|
61
|
+
|
|
62
|
+
function startNewChat() {
|
|
63
|
+
if (busy) return;
|
|
64
|
+
setActiveChatId("");
|
|
65
|
+
setTurns([]);
|
|
66
|
+
setProviderUsed("");
|
|
67
|
+
setInput("");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function openChat(chat: SavedChat) {
|
|
71
|
+
if (busy) return;
|
|
72
|
+
setActiveChatId(chat.id);
|
|
73
|
+
setTurns(chat.turns);
|
|
74
|
+
setProviderUsed("");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function deleteChat(id: string) {
|
|
78
|
+
if (busy) return;
|
|
79
|
+
setHistory((current) => current.filter((chat) => chat.id !== id));
|
|
80
|
+
if (activeChatId === id) startNewChat();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function toggleHistory() {
|
|
84
|
+
setHistoryCollapsed((collapsed) => {
|
|
85
|
+
window.localStorage.setItem("mop-agent-history-collapsed", collapsed ? "0" : "1");
|
|
86
|
+
return !collapsed;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
24
89
|
|
|
25
90
|
async function send(prefill?: string) {
|
|
26
91
|
const message = (prefill ?? input).trim();
|
|
27
92
|
if (!message || busy) return;
|
|
93
|
+
if (!activeChatId) setActiveChatId(`chat-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
|
|
28
94
|
setInput("");
|
|
29
95
|
setBusy(true);
|
|
30
96
|
setTurns((current) => [...current, { role: "user", content: message }, { role: "assistant", content: "" }]);
|
|
@@ -67,65 +133,105 @@ export default function AssistantPage() {
|
|
|
67
133
|
}
|
|
68
134
|
|
|
69
135
|
return (
|
|
70
|
-
<section className=
|
|
71
|
-
<div className="mop-assistant-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
<div
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
136
|
+
<section className={`mop-assistant-page${historyCollapsed ? " is-history-collapsed" : ""}`}>
|
|
137
|
+
<div className="mop-assistant-workspace">
|
|
138
|
+
<div className="mop-assistant-conversation">
|
|
139
|
+
{turns.length === 0 ? (
|
|
140
|
+
<div className="mop-assistant-welcome">
|
|
141
|
+
<div style={assistantLogo}><img src="/icon.svg" alt="MOP-AGENT" /></div>
|
|
142
|
+
<p style={{ color: "#742220", fontSize: 11, fontWeight: 900, letterSpacing: ".16em" }}>MOP-AGENT IS READY</p>
|
|
143
|
+
<h1 style={{ fontFamily: '"SFMono-Regular", Consolas, monospace', fontSize: "clamp(26px, 4vw, 40px)", margin: "8px 0 12px" }}>
|
|
144
|
+
What are we working on, {name.split(" ")[0]}?
|
|
145
|
+
</h1>
|
|
146
|
+
<p style={{ color: "rgba(45,74,62,.7)", maxWidth: 610, lineHeight: 1.65 }}>
|
|
147
|
+
Start talking immediately. Link projects when you want MOP-AGENT to remember their state and work across them.
|
|
148
|
+
</p>
|
|
149
|
+
<div className="mop-prompt-grid" style={promptGrid}>
|
|
150
|
+
{["Help me plan today’s work", "What can MOP-AGENT do?", "Summarize what you remember", "Plan a new software project"].map((prompt) => (
|
|
151
|
+
<button key={prompt} onClick={() => send(prompt)} style={promptCard}>{prompt}<span>→</span></button>
|
|
152
|
+
))}
|
|
153
|
+
</div>
|
|
154
|
+
{projects.length === 0 && (
|
|
155
|
+
<p style={{ fontSize: 13, color: "rgba(45,74,62,.68)", marginTop: 24 }}>
|
|
156
|
+
No project linked yet—this does not block chat. <a href="/brain" style={{ color: "#742220" }}>Link one from Brain →</a>
|
|
157
|
+
</p>
|
|
158
|
+
)}
|
|
159
|
+
</div>
|
|
160
|
+
) : (
|
|
161
|
+
<div style={{ width: "min(100%, 820px)", margin: "0 auto", padding: "28px 0 160px" }}>
|
|
162
|
+
{turns.map((turn, index) => (
|
|
163
|
+
<article key={index} style={{ display: "grid", gridTemplateColumns: "34px 1fr", gap: 13, marginBottom: 26 }}>
|
|
164
|
+
<span style={turn.role === "assistant" ? botAvatar : userAvatar}>{turn.role === "assistant" ? "✦" : name.slice(0, 1).toUpperCase()}</span>
|
|
165
|
+
<div>
|
|
166
|
+
<strong style={{ fontSize: 13, color: turn.role === "assistant" ? "#742220" : "#2d4a3e" }}>{turn.role === "assistant" ? "MOP-AGENT" : "You"}</strong>
|
|
167
|
+
<div style={{ whiteSpace: "pre-wrap", lineHeight: 1.7, marginTop: 6, color: "#2d4a3e" }}>{turn.content || "Thinking…"}</div>
|
|
168
|
+
</div>
|
|
169
|
+
</article>
|
|
85
170
|
))}
|
|
171
|
+
<div ref={endRef} />
|
|
86
172
|
</div>
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
173
|
+
)}
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
<div className="mop-assistant-composer-wrap">
|
|
177
|
+
<div style={composer}>
|
|
178
|
+
<textarea
|
|
179
|
+
value={input}
|
|
180
|
+
onChange={(e) => setInput(e.target.value)}
|
|
181
|
+
onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); } }}
|
|
182
|
+
placeholder="Message MOP-AGENT…"
|
|
183
|
+
rows={1}
|
|
184
|
+
style={textarea}
|
|
185
|
+
/>
|
|
186
|
+
<button onClick={() => send()} disabled={busy || !input.trim()} style={{ ...sendButton, opacity: busy || !input.trim() ? .45 : 1 }}>↑</button>
|
|
92
187
|
</div>
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
{turns.map((turn, index) => (
|
|
96
|
-
<article key={index} style={{ display: "grid", gridTemplateColumns: "34px 1fr", gap: 13, marginBottom: 26 }}>
|
|
97
|
-
<span style={turn.role === "assistant" ? botAvatar : userAvatar}>{turn.role === "assistant" ? "✦" : name.slice(0, 1).toUpperCase()}</span>
|
|
98
|
-
<div>
|
|
99
|
-
<strong style={{ fontSize: 13, color: turn.role === "assistant" ? "#742220" : "#2d4a3e" }}>{turn.role === "assistant" ? "MOP-AGENT" : "You"}</strong>
|
|
100
|
-
<div style={{ whiteSpace: "pre-wrap", lineHeight: 1.7, marginTop: 6, color: "#2d4a3e" }}>{turn.content || "Thinking…"}</div>
|
|
101
|
-
</div>
|
|
102
|
-
</article>
|
|
103
|
-
))}
|
|
104
|
-
<div ref={endRef} />
|
|
188
|
+
<div style={{ textAlign: "center", color: "rgba(45,74,62,.62)", fontSize: 11, marginTop: 8 }}>
|
|
189
|
+
{providerUsed ? `Answered by ${providerUsed} · ` : ""}Cross-project memory
|
|
105
190
|
</div>
|
|
106
|
-
|
|
191
|
+
</div>
|
|
107
192
|
</div>
|
|
108
193
|
|
|
109
|
-
<
|
|
110
|
-
<div
|
|
111
|
-
<
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
194
|
+
<aside className="mop-chat-history" aria-label="Chat history">
|
|
195
|
+
<div className="mop-chat-history-header">
|
|
196
|
+
<strong>Chat history</strong>
|
|
197
|
+
<div className="mop-chat-history-actions">
|
|
198
|
+
<button
|
|
199
|
+
className="mop-chat-history-collapse"
|
|
200
|
+
type="button"
|
|
201
|
+
onClick={toggleHistory}
|
|
202
|
+
aria-label={historyCollapsed ? "Expand chat history" : "Collapse chat history"}
|
|
203
|
+
aria-expanded={!historyCollapsed}
|
|
204
|
+
title={historyCollapsed ? "Expand chat history" : "Collapse chat history"}
|
|
205
|
+
>
|
|
206
|
+
{historyCollapsed ? "‹" : "›"}
|
|
207
|
+
</button>
|
|
208
|
+
<button className="mop-chat-history-new" type="button" onClick={startNewChat} disabled={busy} title="Start a new chat">+</button>
|
|
209
|
+
</div>
|
|
120
210
|
</div>
|
|
121
|
-
<div
|
|
122
|
-
{
|
|
211
|
+
<div className="mop-chat-history-list">
|
|
212
|
+
{history.length === 0 ? (
|
|
213
|
+
<p className="mop-chat-history-empty">Your conversations will appear here.</p>
|
|
214
|
+
) : history.map((chat) => (
|
|
215
|
+
<div className={`mop-chat-history-item${chat.id === activeChatId ? " is-active" : ""}`} key={chat.id}>
|
|
216
|
+
<button type="button" className="mop-chat-history-open" onClick={() => openChat(chat)} disabled={busy}>
|
|
217
|
+
<strong>{chat.title}</strong>
|
|
218
|
+
<span>{formatChatDate(chat.updatedAt)}</span>
|
|
219
|
+
</button>
|
|
220
|
+
<button type="button" className="mop-chat-history-delete" onClick={() => deleteChat(chat.id)} disabled={busy} aria-label={`Delete ${chat.title}`}>×</button>
|
|
221
|
+
</div>
|
|
222
|
+
))}
|
|
123
223
|
</div>
|
|
124
|
-
</
|
|
224
|
+
</aside>
|
|
125
225
|
</section>
|
|
126
226
|
);
|
|
127
227
|
}
|
|
128
228
|
|
|
229
|
+
function formatChatDate(timestamp: number) {
|
|
230
|
+
const date = new Date(timestamp);
|
|
231
|
+
if (Number.isNaN(date.getTime())) return "Saved conversation";
|
|
232
|
+
return new Intl.DateTimeFormat(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }).format(date);
|
|
233
|
+
}
|
|
234
|
+
|
|
129
235
|
const assistantLogo: CSSProperties = { width: 86, height: 86, display: "grid", placeItems: "center" };
|
|
130
236
|
const promptGrid: CSSProperties = { width: "min(100%, 650px)", display: "grid", gridTemplateColumns: "repeat(2,minmax(0,1fr))", gap: 10, marginTop: 28 };
|
|
131
237
|
const promptCard: CSSProperties = { display: "flex", justifyContent: "space-between", padding: "14px 15px", border: "1px solid rgba(45,74,62,.38)", borderBottomWidth: 3, background: "#fffdf2", color: "#2d4a3e", cursor: "pointer", textAlign: "left" };
|
package/apps/web/app/globals.css
CHANGED
|
@@ -121,15 +121,22 @@ button {
|
|
|
121
121
|
box-shadow: 0 2px 0 rgba(45, 74, 62, .28);
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
.mop-app-brand-cell {
|
|
125
|
+
position: relative;
|
|
126
|
+
min-width: 0;
|
|
127
|
+
display: flex;
|
|
128
|
+
border-right: 1px solid rgba(254, 249, 225, .16);
|
|
129
|
+
}
|
|
130
|
+
|
|
124
131
|
.mop-app-brand {
|
|
132
|
+
width: 100%;
|
|
125
133
|
display: flex;
|
|
126
134
|
align-items: center;
|
|
127
135
|
gap: 9px;
|
|
128
136
|
min-width: 0;
|
|
129
|
-
padding: 7px 14px;
|
|
137
|
+
padding: 7px 46px 7px 14px;
|
|
130
138
|
color: var(--mop-cream);
|
|
131
139
|
text-decoration: none;
|
|
132
|
-
border-right: 1px solid rgba(254, 249, 225, .16);
|
|
133
140
|
}
|
|
134
141
|
|
|
135
142
|
.mop-app-brand img {
|
|
@@ -148,6 +155,51 @@ button {
|
|
|
148
155
|
white-space: nowrap;
|
|
149
156
|
}
|
|
150
157
|
|
|
158
|
+
.mop-sidebar-collapse-toggle {
|
|
159
|
+
position: absolute;
|
|
160
|
+
top: 50%;
|
|
161
|
+
right: 9px;
|
|
162
|
+
z-index: 2;
|
|
163
|
+
width: 29px;
|
|
164
|
+
height: 34px;
|
|
165
|
+
display: grid;
|
|
166
|
+
place-items: center;
|
|
167
|
+
padding: 0;
|
|
168
|
+
transform: translateY(-50%);
|
|
169
|
+
border: 1px solid rgba(254, 249, 225, .25);
|
|
170
|
+
background: rgba(254, 249, 225, .08);
|
|
171
|
+
color: var(--mop-cream);
|
|
172
|
+
font-size: 19px;
|
|
173
|
+
cursor: pointer;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.mop-sidebar-collapse-toggle:hover:not(:disabled) { transform: translate(-1px, calc(-50% - 1px)); }
|
|
177
|
+
.mop-sidebar-collapse-toggle:active:not(:disabled) { transform: translate(1px, calc(-50% + 1px)); }
|
|
178
|
+
|
|
179
|
+
.mop-app-frame,
|
|
180
|
+
.mop-app-topbar { transition: grid-template-columns 140ms steps(4, end); }
|
|
181
|
+
|
|
182
|
+
.mop-app-frame.is-sidebar-collapsed { grid-template-columns: 74px minmax(0, 1fr); }
|
|
183
|
+
.mop-app-frame.is-sidebar-collapsed .mop-app-topbar { grid-template-columns: 74px minmax(0, 1fr); }
|
|
184
|
+
.mop-app-frame.is-sidebar-collapsed .mop-app-brand { justify-content: center; padding: 7px; }
|
|
185
|
+
.mop-app-frame.is-sidebar-collapsed .mop-app-brand img { width: 47px; height: 47px; }
|
|
186
|
+
.mop-app-frame.is-sidebar-collapsed .mop-app-brand span,
|
|
187
|
+
.mop-app-frame.is-sidebar-collapsed .mop-nav-section > p,
|
|
188
|
+
.mop-app-frame.is-sidebar-collapsed .mop-sidebar-primary a > span:not(.mop-nav-icon),
|
|
189
|
+
.mop-app-frame.is-sidebar-collapsed .mop-nav-section a > span:not(.mop-nav-icon),
|
|
190
|
+
.mop-app-frame.is-sidebar-collapsed .mop-nav-section button > span:not(.mop-nav-icon),
|
|
191
|
+
.mop-app-frame.is-sidebar-collapsed .mop-account-copy,
|
|
192
|
+
.mop-app-frame.is-sidebar-collapsed .mop-account-card > span:last-child,
|
|
193
|
+
.mop-app-frame.is-sidebar-collapsed .mop-back-workspace-btn > span { display: none; }
|
|
194
|
+
.mop-app-frame.is-sidebar-collapsed .mop-sidebar-collapse-toggle { right: -14px; }
|
|
195
|
+
.mop-app-frame.is-sidebar-collapsed .mop-app-sidebar { padding-inline: 9px; }
|
|
196
|
+
.mop-app-frame.is-sidebar-collapsed .mop-sidebar-primary a,
|
|
197
|
+
.mop-app-frame.is-sidebar-collapsed .mop-nav-section a,
|
|
198
|
+
.mop-app-frame.is-sidebar-collapsed .mop-nav-section button,
|
|
199
|
+
.mop-app-frame.is-sidebar-collapsed .mop-account-card { justify-content: center; padding-inline: 5px; }
|
|
200
|
+
.mop-app-frame.is-sidebar-collapsed .mop-nav-icon { width: auto; }
|
|
201
|
+
.mop-app-frame.is-sidebar-collapsed .mop-back-workspace-btn::before { content: "←"; }
|
|
202
|
+
|
|
151
203
|
.mop-app-topbar-main {
|
|
152
204
|
position: relative;
|
|
153
205
|
min-width: 0;
|
|
@@ -272,31 +324,7 @@ button {
|
|
|
272
324
|
font-size: 16px;
|
|
273
325
|
}
|
|
274
326
|
|
|
275
|
-
.mop-project-memory {
|
|
276
|
-
min-height: 0;
|
|
277
|
-
overflow: hidden;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
.mop-project-memory nav {
|
|
281
|
-
max-height: min(34vh, 280px);
|
|
282
|
-
overflow-y: auto;
|
|
283
|
-
scrollbar-width: thin;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
.mop-project-memory a { min-height: 34px; padding-block: 5px; font-size: 13px; }
|
|
287
|
-
.mop-project-dot {
|
|
288
|
-
flex: 0 0 auto;
|
|
289
|
-
width: 7px;
|
|
290
|
-
height: 7px;
|
|
291
|
-
background: rgba(254, 249, 225, .34);
|
|
292
|
-
}
|
|
293
|
-
.mop-project-dot[data-online="true"] { background: #78e19b; box-shadow: 0 0 0 2px rgba(120, 225, 155, .12); }
|
|
294
|
-
.mop-nav-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
295
|
-
.mop-sidebar-empty { display: block; padding: 7px 11px; color: rgba(254, 249, 225, .42); font-size: 12px; }
|
|
296
327
|
.mop-admin-nav { margin-top: 2px; }
|
|
297
|
-
.mop-settings-subnav { display: grid; gap: 1px; margin: 1px 0 2px 31px; border-left: 1px solid rgba(254, 249, 225, .18); }
|
|
298
|
-
.mop-settings-subnav button { min-height: 32px; padding: 5px 12px; color: rgba(254, 249, 225, .6); font-size: 12px; }
|
|
299
|
-
.mop-settings-subnav button.is-active { color: #ff9a56; border-color: transparent; background: transparent; box-shadow: none; }
|
|
300
328
|
|
|
301
329
|
.mop-sidebar-spacer { flex: 1; }
|
|
302
330
|
.mop-account-card {
|
|
@@ -389,7 +417,20 @@ button {
|
|
|
389
417
|
|
|
390
418
|
/* Assistant content now lives inside the shared shell. */
|
|
391
419
|
.mop-assistant-page {
|
|
392
|
-
|
|
420
|
+
height: calc(100vh - 70px);
|
|
421
|
+
min-height: 560px;
|
|
422
|
+
position: relative;
|
|
423
|
+
display: grid;
|
|
424
|
+
grid-template-columns: minmax(0, 1fr) 290px;
|
|
425
|
+
overflow: hidden;
|
|
426
|
+
transition: grid-template-columns 140ms steps(4, end);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
.mop-assistant-page.is-history-collapsed { grid-template-columns: minmax(0, 1fr) 52px; }
|
|
430
|
+
|
|
431
|
+
.mop-assistant-workspace {
|
|
432
|
+
min-width: 0;
|
|
433
|
+
min-height: 0;
|
|
393
434
|
position: relative;
|
|
394
435
|
display: flex;
|
|
395
436
|
flex-direction: column;
|
|
@@ -417,6 +458,110 @@ button {
|
|
|
417
458
|
background: linear-gradient(transparent, var(--mop-cream) 28%);
|
|
418
459
|
}
|
|
419
460
|
|
|
461
|
+
.mop-chat-history {
|
|
462
|
+
min-width: 0;
|
|
463
|
+
min-height: 0;
|
|
464
|
+
display: flex;
|
|
465
|
+
flex-direction: column;
|
|
466
|
+
padding: 17px 13px 13px;
|
|
467
|
+
color: var(--mop-green);
|
|
468
|
+
border-left: 1px solid rgba(45, 74, 62, .3);
|
|
469
|
+
background:
|
|
470
|
+
linear-gradient(rgba(255, 253, 242, .84), rgba(254, 249, 225, .94)),
|
|
471
|
+
repeating-linear-gradient(0deg, transparent 0, transparent 7px, rgba(116, 34, 32, .04) 7px, rgba(116, 34, 32, .04) 8px);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
.mop-chat-history-header {
|
|
475
|
+
display: flex;
|
|
476
|
+
align-items: center;
|
|
477
|
+
justify-content: space-between;
|
|
478
|
+
gap: 12px;
|
|
479
|
+
padding: 2px 2px 14px;
|
|
480
|
+
border-bottom: 1px solid rgba(45, 74, 62, .24);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
.mop-chat-history-header strong { font-family: "SFMono-Regular", Consolas, monospace; font-size: 15px; }
|
|
484
|
+
.mop-chat-history-actions { display: flex; gap: 6px; }
|
|
485
|
+
.mop-chat-history-header button {
|
|
486
|
+
width: 34px;
|
|
487
|
+
height: 34px;
|
|
488
|
+
border: 1px solid rgba(45, 74, 62, .38);
|
|
489
|
+
background: var(--mop-red);
|
|
490
|
+
color: var(--mop-cream);
|
|
491
|
+
font-size: 20px;
|
|
492
|
+
cursor: pointer;
|
|
493
|
+
}
|
|
494
|
+
.mop-chat-history-collapse { background: var(--mop-green) !important; }
|
|
495
|
+
.mop-chat-history-new { background: var(--mop-red) !important; }
|
|
496
|
+
|
|
497
|
+
.mop-assistant-page.is-history-collapsed .mop-chat-history { padding: 12px 8px; }
|
|
498
|
+
.mop-assistant-page.is-history-collapsed .mop-chat-history-header {
|
|
499
|
+
justify-content: center;
|
|
500
|
+
padding: 0 0 12px;
|
|
501
|
+
border-bottom: 0;
|
|
502
|
+
}
|
|
503
|
+
.mop-assistant-page.is-history-collapsed .mop-chat-history-header > strong,
|
|
504
|
+
.mop-assistant-page.is-history-collapsed .mop-chat-history-new,
|
|
505
|
+
.mop-assistant-page.is-history-collapsed .mop-chat-history-list { display: none; }
|
|
506
|
+
.mop-assistant-page.is-history-collapsed .mop-chat-history-actions { display: block; }
|
|
507
|
+
|
|
508
|
+
.mop-chat-history-list {
|
|
509
|
+
min-height: 0;
|
|
510
|
+
display: grid;
|
|
511
|
+
align-content: start;
|
|
512
|
+
gap: 6px;
|
|
513
|
+
overflow-y: auto;
|
|
514
|
+
padding: 12px 1px;
|
|
515
|
+
scrollbar-width: thin;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
.mop-chat-history-empty {
|
|
519
|
+
margin: 8px 5px;
|
|
520
|
+
color: rgba(45, 74, 62, .58);
|
|
521
|
+
font-size: 12px;
|
|
522
|
+
line-height: 1.55;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
.mop-chat-history-item {
|
|
526
|
+
display: grid;
|
|
527
|
+
grid-template-columns: minmax(0, 1fr) 28px;
|
|
528
|
+
align-items: stretch;
|
|
529
|
+
border: 1px solid transparent;
|
|
530
|
+
}
|
|
531
|
+
.mop-chat-history-item:hover,
|
|
532
|
+
.mop-chat-history-item.is-active {
|
|
533
|
+
border-color: rgba(45, 74, 62, .24);
|
|
534
|
+
background: rgba(255, 253, 242, .78);
|
|
535
|
+
}
|
|
536
|
+
.mop-chat-history-item.is-active { border-left: 3px solid var(--mop-red); }
|
|
537
|
+
.mop-chat-history-open,
|
|
538
|
+
.mop-chat-history-delete {
|
|
539
|
+
min-width: 0;
|
|
540
|
+
border: 0;
|
|
541
|
+
box-shadow: none;
|
|
542
|
+
background: transparent;
|
|
543
|
+
color: var(--mop-green);
|
|
544
|
+
cursor: pointer;
|
|
545
|
+
}
|
|
546
|
+
.mop-chat-history-open {
|
|
547
|
+
display: grid;
|
|
548
|
+
gap: 4px;
|
|
549
|
+
padding: 9px 7px 9px 9px;
|
|
550
|
+
text-align: left;
|
|
551
|
+
}
|
|
552
|
+
.mop-chat-history-open strong {
|
|
553
|
+
overflow: hidden;
|
|
554
|
+
text-overflow: ellipsis;
|
|
555
|
+
white-space: nowrap;
|
|
556
|
+
font-size: 12px;
|
|
557
|
+
font-weight: 760;
|
|
558
|
+
}
|
|
559
|
+
.mop-chat-history-open span { color: rgba(45, 74, 62, .5); font-size: 9px; }
|
|
560
|
+
.mop-chat-history-delete { opacity: 0; padding: 0; font-size: 17px; }
|
|
561
|
+
.mop-chat-history-item:hover .mop-chat-history-delete,
|
|
562
|
+
.mop-chat-history-item.is-active .mop-chat-history-delete { opacity: .65; }
|
|
563
|
+
.mop-chat-history-delete:hover { color: var(--mop-red); opacity: 1 !important; }
|
|
564
|
+
|
|
420
565
|
.mop-settings-grid {
|
|
421
566
|
display: grid;
|
|
422
567
|
grid-template-columns: 1fr;
|
|
@@ -457,7 +602,10 @@ button {
|
|
|
457
602
|
grid-template-rows: 62px minmax(0, 1fr);
|
|
458
603
|
grid-template-areas: "topbar" "main";
|
|
459
604
|
}
|
|
605
|
+
.mop-app-frame.is-sidebar-collapsed { grid-template-columns: 1fr; }
|
|
460
606
|
.mop-app-topbar { grid-template-columns: 66px minmax(0, 1fr); }
|
|
607
|
+
.mop-app-frame.is-sidebar-collapsed .mop-app-topbar { grid-template-columns: 66px minmax(0, 1fr); }
|
|
608
|
+
.mop-sidebar-collapse-toggle { display: none; }
|
|
461
609
|
.mop-app-brand { justify-content: center; padding: 5px; }
|
|
462
610
|
.mop-app-brand img { width: 49px; height: 49px; }
|
|
463
611
|
.mop-app-brand span { display: none; }
|
|
@@ -475,6 +623,18 @@ button {
|
|
|
475
623
|
transition: transform 160ms steps(4, end);
|
|
476
624
|
}
|
|
477
625
|
.mop-app-sidebar.is-open { transform: translateX(0); }
|
|
626
|
+
.mop-app-frame.is-sidebar-collapsed .mop-nav-section > p { display: block; }
|
|
627
|
+
.mop-app-frame.is-sidebar-collapsed .mop-sidebar-primary a > span:not(.mop-nav-icon),
|
|
628
|
+
.mop-app-frame.is-sidebar-collapsed .mop-nav-section a > span:not(.mop-nav-icon),
|
|
629
|
+
.mop-app-frame.is-sidebar-collapsed .mop-nav-section button > span:not(.mop-nav-icon),
|
|
630
|
+
.mop-app-frame.is-sidebar-collapsed .mop-back-workspace-btn > span { display: inline; }
|
|
631
|
+
.mop-app-frame.is-sidebar-collapsed .mop-account-copy { display: grid; }
|
|
632
|
+
.mop-app-frame.is-sidebar-collapsed .mop-account-card > span:last-child { display: inline; }
|
|
633
|
+
.mop-app-frame.is-sidebar-collapsed .mop-sidebar-primary a,
|
|
634
|
+
.mop-app-frame.is-sidebar-collapsed .mop-nav-section a,
|
|
635
|
+
.mop-app-frame.is-sidebar-collapsed .mop-nav-section button,
|
|
636
|
+
.mop-app-frame.is-sidebar-collapsed .mop-account-card { justify-content: flex-start; padding-inline: 11px; }
|
|
637
|
+
.mop-app-frame.is-sidebar-collapsed .mop-back-workspace-btn::before { content: none; }
|
|
478
638
|
.mop-sidebar-scrim {
|
|
479
639
|
display: block;
|
|
480
640
|
position: fixed;
|
|
@@ -484,9 +644,36 @@ button {
|
|
|
484
644
|
background: rgba(20, 34, 29, .56);
|
|
485
645
|
}
|
|
486
646
|
.mop-app-main { min-height: calc(100vh - 62px); }
|
|
487
|
-
.mop-assistant-page
|
|
647
|
+
.mop-assistant-page,
|
|
648
|
+
.mop-assistant-page.is-history-collapsed { height: calc(100vh - 62px); min-height: 520px; grid-template-columns: minmax(0, 1fr); }
|
|
649
|
+
.mop-chat-history { display: none; }
|
|
488
650
|
.mop-assistant-conversation { padding: 0 16px; }
|
|
489
651
|
.mop-assistant-composer-wrap { padding: 26px 12px 12px; }
|
|
490
652
|
.mop-settings-grid { grid-template-columns: 1fr; }
|
|
491
653
|
.mop-user-invite-form { grid-template-columns: 1fr !important; }
|
|
492
654
|
}
|
|
655
|
+
|
|
656
|
+
.mop-back-workspace-btn {
|
|
657
|
+
display: flex;
|
|
658
|
+
align-items: center;
|
|
659
|
+
justify-content: center;
|
|
660
|
+
width: 100%;
|
|
661
|
+
min-height: 40px;
|
|
662
|
+
margin-bottom: 9px;
|
|
663
|
+
padding: 9px 12px;
|
|
664
|
+
border: 1px solid var(--mop-red);
|
|
665
|
+
background: var(--mop-red);
|
|
666
|
+
color: var(--mop-cream);
|
|
667
|
+
font-family: "SFMono-Regular", Consolas, monospace;
|
|
668
|
+
font-size: 11px;
|
|
669
|
+
font-weight: 900;
|
|
670
|
+
text-decoration: none;
|
|
671
|
+
box-shadow: 2px 2px 0 rgba(18, 38, 30, .28);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
.mop-back-workspace-btn:hover { color: var(--mop-cream); transform: translate(-1px, -1px); }
|
|
675
|
+
|
|
676
|
+
@media (min-width: 761px) and (max-width: 1050px) {
|
|
677
|
+
.mop-assistant-page { grid-template-columns: minmax(0, 1fr) 235px; }
|
|
678
|
+
.mop-chat-history { padding-inline: 9px; }
|
|
679
|
+
}
|
|
@@ -40,6 +40,7 @@ function pageTitle(pathname: string): string {
|
|
|
40
40
|
export function AppShell({ viewer, children }: { viewer: AppViewer; children: ReactNode }) {
|
|
41
41
|
const pathname = usePathname();
|
|
42
42
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
43
|
+
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
|
43
44
|
const [projects, setProjects] = useState<Project[]>([]);
|
|
44
45
|
const [settingsSection, setSettingsSection] = useState<"providers" | "users">("providers");
|
|
45
46
|
const isAdmin = viewer.role === "owner";
|
|
@@ -54,6 +55,7 @@ export function AppShell({ viewer, children }: { viewer: AppViewer; children: Re
|
|
|
54
55
|
|
|
55
56
|
const requested = new URLSearchParams(window.location.search).get("section");
|
|
56
57
|
if (requested === "users") setSettingsSection("users");
|
|
58
|
+
setSidebarCollapsed(window.localStorage.getItem("mop-agent-sidebar-collapsed") === "1");
|
|
57
59
|
}, []);
|
|
58
60
|
|
|
59
61
|
async function logout() {
|
|
@@ -66,14 +68,33 @@ export function AppShell({ viewer, children }: { viewer: AppViewer; children: Re
|
|
|
66
68
|
window.history.replaceState(null, "", section === "providers" ? "/settings" : "/settings?section=users");
|
|
67
69
|
}
|
|
68
70
|
|
|
71
|
+
function toggleSidebar() {
|
|
72
|
+
setSidebarCollapsed((collapsed) => {
|
|
73
|
+
window.localStorage.setItem("mop-agent-sidebar-collapsed", collapsed ? "0" : "1");
|
|
74
|
+
return !collapsed;
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
69
78
|
return (
|
|
70
79
|
<MemoryCoreContext.Provider value={{ projects, settingsSection, setSettingsSection }}>
|
|
71
|
-
<div className=
|
|
80
|
+
<div className={`mop-app-frame${sidebarCollapsed ? " is-sidebar-collapsed" : ""}`}>
|
|
72
81
|
<header className="mop-app-topbar">
|
|
73
|
-
<
|
|
74
|
-
<
|
|
75
|
-
|
|
76
|
-
|
|
82
|
+
<div className="mop-app-brand-cell">
|
|
83
|
+
<a className="mop-app-brand" href="/assistant" aria-label="MOP-AGENT home">
|
|
84
|
+
<img src="/icon.svg" alt="" />
|
|
85
|
+
<span>MOP-AGENT</span>
|
|
86
|
+
</a>
|
|
87
|
+
<button
|
|
88
|
+
className="mop-sidebar-collapse-toggle"
|
|
89
|
+
type="button"
|
|
90
|
+
aria-label={sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"}
|
|
91
|
+
aria-expanded={!sidebarCollapsed}
|
|
92
|
+
title={sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"}
|
|
93
|
+
onClick={toggleSidebar}
|
|
94
|
+
>
|
|
95
|
+
{sidebarCollapsed ? "›" : "‹"}
|
|
96
|
+
</button>
|
|
97
|
+
</div>
|
|
77
98
|
<div className="mop-app-topbar-main">
|
|
78
99
|
<button
|
|
79
100
|
className="mop-menu-toggle"
|
|
@@ -94,53 +115,53 @@ export function AppShell({ viewer, children }: { viewer: AppViewer; children: Re
|
|
|
94
115
|
{menuOpen && <button className="mop-sidebar-scrim" aria-label="Close navigation" onClick={() => setMenuOpen(false)} />}
|
|
95
116
|
|
|
96
117
|
<aside className={`mop-app-sidebar${menuOpen ? " is-open" : ""}`}>
|
|
97
|
-
|
|
98
|
-
<
|
|
99
|
-
<
|
|
100
|
-
<span>New chat</span>
|
|
101
|
-
</a>
|
|
102
|
-
<a href="/brain" className={pathname.startsWith("/brain") ? "is-active" : ""} onClick={() => setMenuOpen(false)}>
|
|
103
|
-
<span className="mop-nav-icon">◉</span>
|
|
104
|
-
<span>Brain</span>
|
|
105
|
-
</a>
|
|
106
|
-
</nav>
|
|
107
|
-
|
|
108
|
-
<div className="mop-nav-section mop-project-memory">
|
|
109
|
-
<p>PROJECT MEMORY</p>
|
|
110
|
-
<nav>
|
|
111
|
-
{projects.slice(0, 8).map((project) => (
|
|
112
|
-
<a key={project.id} href={`/brain/${project.id}`} className={pathname === `/brain/${project.id}` ? "is-active" : ""} onClick={() => setMenuOpen(false)}>
|
|
113
|
-
<span className="mop-project-dot" data-online={project.status === "online"} />
|
|
114
|
-
<span className="mop-nav-label">{project.name}</span>
|
|
115
|
-
</a>
|
|
116
|
-
))}
|
|
117
|
-
{projects.length === 0 && <span className="mop-sidebar-empty">No linked projects yet</span>}
|
|
118
|
-
</nav>
|
|
119
|
-
</div>
|
|
120
|
-
|
|
121
|
-
{isAdmin && (
|
|
122
|
-
<div className="mop-nav-section mop-admin-nav">
|
|
123
|
-
<p>ADMIN</p>
|
|
118
|
+
{isSettings ? (
|
|
119
|
+
<div className="mop-nav-section">
|
|
120
|
+
<p>SETTINGS</p>
|
|
124
121
|
<nav>
|
|
125
|
-
<
|
|
126
|
-
<span className="mop-nav-icon"
|
|
127
|
-
<span>
|
|
128
|
-
</
|
|
129
|
-
{
|
|
130
|
-
<
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
</button>
|
|
134
|
-
<button className={settingsSection === "users" ? "is-active" : ""} onClick={() => { selectSection("users"); setMenuOpen(false); }}>
|
|
135
|
-
<span>Users</span>
|
|
136
|
-
</button>
|
|
137
|
-
</div>
|
|
138
|
-
)}
|
|
122
|
+
<button className={settingsSection === "providers" ? "is-active" : ""} onClick={() => { selectSection("providers"); setMenuOpen(false); }}>
|
|
123
|
+
<span className="mop-nav-icon">◇</span>
|
|
124
|
+
<span>Providers</span>
|
|
125
|
+
</button>
|
|
126
|
+
<button className={settingsSection === "users" ? "is-active" : ""} onClick={() => { selectSection("users"); setMenuOpen(false); }}>
|
|
127
|
+
<span className="mop-nav-icon">♙</span>
|
|
128
|
+
<span>Users</span>
|
|
129
|
+
</button>
|
|
139
130
|
</nav>
|
|
140
131
|
</div>
|
|
132
|
+
) : (
|
|
133
|
+
<>
|
|
134
|
+
<nav className="mop-sidebar-primary" aria-label="Workspace">
|
|
135
|
+
<a href="/assistant" className={pathname.startsWith("/assistant") || pathname.startsWith("/chat/") ? "is-active" : ""} onClick={() => setMenuOpen(false)}>
|
|
136
|
+
<span className="mop-nav-icon">✎</span>
|
|
137
|
+
<span>New chat</span>
|
|
138
|
+
</a>
|
|
139
|
+
<a href="/brain" className={pathname.startsWith("/brain") ? "is-active" : ""} onClick={() => setMenuOpen(false)}>
|
|
140
|
+
<span className="mop-nav-icon">◉</span>
|
|
141
|
+
<span>Brain</span>
|
|
142
|
+
</a>
|
|
143
|
+
</nav>
|
|
144
|
+
|
|
145
|
+
{isAdmin && (
|
|
146
|
+
<div className="mop-nav-section mop-admin-nav">
|
|
147
|
+
<p>ADMIN</p>
|
|
148
|
+
<nav>
|
|
149
|
+
<a href="/settings" onClick={() => setMenuOpen(false)}>
|
|
150
|
+
<span className="mop-nav-icon">⚙</span>
|
|
151
|
+
<span>Settings</span>
|
|
152
|
+
</a>
|
|
153
|
+
</nav>
|
|
154
|
+
</div>
|
|
155
|
+
)}
|
|
156
|
+
</>
|
|
141
157
|
)}
|
|
142
158
|
|
|
143
159
|
<div className="mop-sidebar-spacer" />
|
|
160
|
+
{isSettings && (
|
|
161
|
+
<a href="/assistant" className="mop-back-workspace-btn" onClick={() => setMenuOpen(false)}>
|
|
162
|
+
<span>← BACK TO WORKSPACE</span>
|
|
163
|
+
</a>
|
|
164
|
+
)}
|
|
144
165
|
<button className="mop-account-card" type="button" onClick={logout} title="Sign out">
|
|
145
166
|
<span className="mop-account-avatar">{viewer.name.slice(0, 1).toUpperCase()}</span>
|
|
146
167
|
<span className="mop-account-copy">
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mop-agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "mop-agent",
|
|
9
|
-
"version": "0.1.
|
|
9
|
+
"version": "0.1.15",
|
|
10
10
|
"license": "UNLICENSED",
|
|
11
11
|
"workspaces": [
|
|
12
12
|
"packages/*",
|
package/package.json
CHANGED