@pheem49/mint 1.7.1 → 1.8.0
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/Cargo.lock +3 -3
- package/Cargo.toml +1 -1
- package/README.md +9 -0
- package/crates/mint-core/src/api_server.rs +62 -4
- package/crates/mint-core/src/chat.rs +79 -0
- package/crates/mint-core/src/lib.rs +1 -1
- package/crates/mint-core/src/workflows.rs +8 -0
- package/package.json +2 -1
- package/src/renderer/src/App.tsx +1 -1
- package/src/renderer/src/components/ChatPanel.tsx +27 -14
- package/src/renderer/src/components/DashboardSidebar.tsx +71 -12
- package/src/renderer/src/components/ImageStudioPanel.tsx +7 -3
- package/src/renderer/src/components/MintDashboard.tsx +199 -5
- package/src/renderer/src/components/PicturesLibrary.tsx +1 -3
- package/src/renderer/src/components/Settings/AudioTab.tsx +97 -0
- package/src/renderer/src/components/Settings/AutomationTab.tsx +195 -0
- package/src/renderer/src/components/Settings/GeneralTab.tsx +950 -0
- package/src/renderer/src/components/Settings/MemoryTab.tsx +60 -0
- package/src/renderer/src/components/Settings/PluginsTab.tsx +452 -0
- package/src/renderer/src/components/Settings/ShortcutsTab.tsx +30 -0
- package/src/renderer/src/components/Settings/ThemeTab.tsx +129 -0
- package/src/renderer/src/components/SettingsWindow.tsx +175 -962
- package/src/renderer/src/components/WorkflowBuilderPanel.tsx +536 -0
- package/src/renderer/src/components/WorkflowsWindow.tsx +46 -0
- package/src/renderer/src/css/image-studio.css +9 -4
- package/src/renderer/src/css/settings.css +75 -6
- package/src/renderer/src/css/styles.css +355 -38
- package/src/renderer/src/css/workflows.css +542 -0
- package/src/renderer/src/env.d.ts +2 -0
- package/src/renderer/src/index.css +1 -0
- package/src/renderer/src/tauri.ts +97 -0
- package/src/renderer/src-web/components/ChatPanel.tsx +27 -14
- package/src/renderer/src-web/components/DashboardSidebar.tsx +17 -1
- package/src/renderer/src-web/components/ImageStudioPanel.tsx +7 -3
- package/src/renderer/src-web/components/MintDashboard.tsx +189 -0
- package/src/renderer/src-web/components/PicturesLibrary.tsx +1 -3
- package/src/renderer/src-web/components/Settings/AudioTab.tsx +97 -0
- package/src/renderer/src-web/components/Settings/AutomationTab.tsx +196 -0
- package/src/renderer/src-web/components/Settings/GeneralTab.tsx +948 -0
- package/src/renderer/src-web/components/Settings/MemoryTab.tsx +60 -0
- package/src/renderer/src-web/components/Settings/PluginsTab.tsx +452 -0
- package/src/renderer/src-web/components/Settings/ShortcutsTab.tsx +30 -0
- package/src/renderer/src-web/components/Settings/ThemeTab.tsx +129 -0
- package/src/renderer/src-web/components/SettingsWindow.tsx +187 -968
- package/src/renderer/src-web/css/image-studio.css +9 -4
- package/src/renderer/src-web/css/settings.css +75 -6
- package/src/renderer/src-web/css/styles.css +294 -38
- package/src/renderer/src-web/env.d.ts +2 -0
- package/src/renderer/src-web/tauri.ts +36 -0
- package/src-tauri/src/lib.rs +83 -90
- package/src-tauri/tauri.conf.json +1 -1
package/Cargo.lock
CHANGED
|
@@ -2095,7 +2095,7 @@ dependencies = [
|
|
|
2095
2095
|
|
|
2096
2096
|
[[package]]
|
|
2097
2097
|
name = "mint-cli"
|
|
2098
|
-
version = "1.
|
|
2098
|
+
version = "1.8.0"
|
|
2099
2099
|
dependencies = [
|
|
2100
2100
|
"anyhow",
|
|
2101
2101
|
"base64 0.22.1",
|
|
@@ -2113,7 +2113,7 @@ dependencies = [
|
|
|
2113
2113
|
|
|
2114
2114
|
[[package]]
|
|
2115
2115
|
name = "mint-core"
|
|
2116
|
-
version = "1.
|
|
2116
|
+
version = "1.8.0"
|
|
2117
2117
|
dependencies = [
|
|
2118
2118
|
"base64 0.22.1",
|
|
2119
2119
|
"chrono",
|
|
@@ -2134,7 +2134,7 @@ dependencies = [
|
|
|
2134
2134
|
|
|
2135
2135
|
[[package]]
|
|
2136
2136
|
name = "mint-desktop"
|
|
2137
|
-
version = "1.
|
|
2137
|
+
version = "1.8.0"
|
|
2138
2138
|
dependencies = [
|
|
2139
2139
|
"base64 0.22.1",
|
|
2140
2140
|
"dirs",
|
package/Cargo.toml
CHANGED
package/README.md
CHANGED
|
@@ -95,6 +95,15 @@ Mint is a local-first AI assistant running on your machine, capable of handling
|
|
|
95
95
|
- Telegram, Discord Gateway, Discord RPC, Slack Socket Mode, LINE, and WhatsApp
|
|
96
96
|
Cloud API integrations.
|
|
97
97
|
- Signed Tauri update checks with an explicit approval step before installation.
|
|
98
|
+
- Dynamic local Ollama model fetching in the Settings Window to query and display the actual models installed on your machine.
|
|
99
|
+
- Pill-styled clean horizontal system event dividers for provider and model change notifications in the chat panel.
|
|
100
|
+
|
|
101
|
+
## What's New in v1.8.0
|
|
102
|
+
|
|
103
|
+
- **Integrated Web Search Settings**: Easily toggle between **Brave Search API** and **Google Custom Search API** directly from the General settings tab.
|
|
104
|
+
- **Image Generation Providers**: Choose and configure credentials for Stability AI, Ideogram, Replicate, OpenAI DALL-E, and Google NanoBanana (Gemini Images) inside dedicated inline settings cards.
|
|
105
|
+
- **Inline Productivity Settings**: Set up Client IDs, secrets, and API keys for native Gmail, Google Calendar, and Notion integrations inline inside the General settings tab.
|
|
106
|
+
- **Revamped Plugins Layout**: Moved **Learned AI Skills** (equivalent to `mint learn` in CLI) to the top of the Plugins tab, followed by MCP Servers (External tools), and Built-in Plugins (Spotify and Discord RPC) at the bottom.
|
|
98
107
|
|
|
99
108
|
## <img src="assets/setup.svg" width="24" height="24" valign="middle" /> Prerequisites
|
|
100
109
|
|
|
@@ -272,6 +272,58 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
272
272
|
.await;
|
|
273
273
|
}
|
|
274
274
|
}
|
|
275
|
+
("POST", "/api/interactions") => {
|
|
276
|
+
#[derive(Deserialize)]
|
|
277
|
+
#[serde(rename_all = "camelCase")]
|
|
278
|
+
struct ApiSaveInteraction {
|
|
279
|
+
chat_id: String,
|
|
280
|
+
user_text: String,
|
|
281
|
+
provider: String,
|
|
282
|
+
model: String,
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if let Ok(req) = serde_json::from_str::<ApiSaveInteraction>(body) {
|
|
286
|
+
if let Ok(memory) = MemoryStore::open_default() {
|
|
287
|
+
match memory.add_interaction_for_chat(
|
|
288
|
+
&req.chat_id,
|
|
289
|
+
&req.user_text,
|
|
290
|
+
"",
|
|
291
|
+
&req.provider,
|
|
292
|
+
&req.model,
|
|
293
|
+
) {
|
|
294
|
+
Ok(row_id) => {
|
|
295
|
+
let res_json = json!({ "success": true, "id": row_id });
|
|
296
|
+
send_json_response(socket, "200 OK", &res_json.to_string())
|
|
297
|
+
.await;
|
|
298
|
+
}
|
|
299
|
+
Err(error) => {
|
|
300
|
+
let err_json =
|
|
301
|
+
json!({ "success": false, "message": error.to_string() });
|
|
302
|
+
send_json_response(
|
|
303
|
+
socket,
|
|
304
|
+
"500 Internal Server Error",
|
|
305
|
+
&err_json.to_string(),
|
|
306
|
+
)
|
|
307
|
+
.await;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
} else {
|
|
311
|
+
send_json_response(
|
|
312
|
+
socket,
|
|
313
|
+
"500 Internal Server Error",
|
|
314
|
+
"{\"success\":false,\"message\":\"db error\"}",
|
|
315
|
+
)
|
|
316
|
+
.await;
|
|
317
|
+
}
|
|
318
|
+
} else {
|
|
319
|
+
send_json_response(
|
|
320
|
+
socket,
|
|
321
|
+
"400 Bad Request",
|
|
322
|
+
"{\"success\":false,\"message\":\"invalid body\"}",
|
|
323
|
+
)
|
|
324
|
+
.await;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
275
327
|
("POST", "/api/interactions/agent-activity") => {
|
|
276
328
|
#[derive(Deserialize)]
|
|
277
329
|
#[serde(rename_all = "camelCase")]
|
|
@@ -414,19 +466,22 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
414
466
|
chat_id: Option<String>,
|
|
415
467
|
image_data_uri: Option<String>,
|
|
416
468
|
audio_data_uri: Option<String>,
|
|
469
|
+
document_attachment: Option<crate::chat::DocumentAttachment>,
|
|
417
470
|
}
|
|
418
471
|
|
|
419
472
|
if let Ok(req) = serde_json::from_str::<ApiChatRequest>(body) {
|
|
420
473
|
let config = load_config().unwrap_or_default();
|
|
421
|
-
let
|
|
474
|
+
let chat_req = ChatRequest {
|
|
422
475
|
message: req.message,
|
|
423
476
|
system_instruction: req.system_instruction.unwrap_or_default(),
|
|
424
477
|
chat_id: req.chat_id,
|
|
425
478
|
image_data_uri: req.image_data_uri,
|
|
426
479
|
audio_data_uri: req.audio_data_uri,
|
|
427
|
-
document_attachment:
|
|
480
|
+
document_attachment: req.document_attachment,
|
|
428
481
|
workspace_path: None,
|
|
429
482
|
};
|
|
483
|
+
let mut chat_req =
|
|
484
|
+
chat_req.with_document_context(&config).unwrap_or(chat_req);
|
|
430
485
|
let sent_image = chat_req.image_data_uri.clone();
|
|
431
486
|
let sent_message = chat_req.message.clone();
|
|
432
487
|
|
|
@@ -495,19 +550,22 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
495
550
|
chat_id: Option<String>,
|
|
496
551
|
image_data_uri: Option<String>,
|
|
497
552
|
audio_data_uri: Option<String>,
|
|
553
|
+
document_attachment: Option<crate::chat::DocumentAttachment>,
|
|
498
554
|
}
|
|
499
555
|
|
|
500
556
|
if let Ok(req) = serde_json::from_str::<ApiChatRequest>(body) {
|
|
501
557
|
let config = load_config().unwrap_or_default();
|
|
502
|
-
let
|
|
558
|
+
let chat_req = ChatRequest {
|
|
503
559
|
message: req.message,
|
|
504
560
|
system_instruction: req.system_instruction.unwrap_or_default(),
|
|
505
561
|
chat_id: req.chat_id,
|
|
506
562
|
image_data_uri: req.image_data_uri,
|
|
507
563
|
audio_data_uri: req.audio_data_uri,
|
|
508
|
-
document_attachment:
|
|
564
|
+
document_attachment: req.document_attachment,
|
|
509
565
|
workspace_path: None,
|
|
510
566
|
};
|
|
567
|
+
let mut chat_req =
|
|
568
|
+
chat_req.with_document_context(&config).unwrap_or(chat_req);
|
|
511
569
|
let sent_image = chat_req.image_data_uri.clone();
|
|
512
570
|
let sent_message = chat_req.message.clone();
|
|
513
571
|
|
|
@@ -108,6 +108,85 @@ pub struct DocumentAttachment {
|
|
|
108
108
|
pub data_uri: String,
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
const MAX_DOCUMENT_BYTES: usize = 10 * 1024 * 1024;
|
|
112
|
+
const MAX_DOCUMENT_CONTEXT_CHARS: usize = 400_000;
|
|
113
|
+
static COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
|
|
114
|
+
|
|
115
|
+
impl ChatRequest {
|
|
116
|
+
pub fn with_document_context(&self, config: &crate::MintConfig) -> Result<ChatRequest, String> {
|
|
117
|
+
let Some(document) = &self.document_attachment else {
|
|
118
|
+
return Ok(self.clone());
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
let (mime_type, encoded) = document
|
|
122
|
+
.data_uri
|
|
123
|
+
.strip_prefix("data:")
|
|
124
|
+
.and_then(|payload| payload.split_once(";base64,"))
|
|
125
|
+
.ok_or_else(|| "invalid PDF attachment data URI".to_string())?;
|
|
126
|
+
|
|
127
|
+
if !mime_type.eq_ignore_ascii_case("application/pdf") {
|
|
128
|
+
return Err("only PDF document attachments are supported".into());
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD};
|
|
132
|
+
let bytes = BASE64_STANDARD
|
|
133
|
+
.decode(encoded)
|
|
134
|
+
.map_err(|error| format!("invalid PDF attachment encoding: {error}"))?;
|
|
135
|
+
if bytes.len() > MAX_DOCUMENT_BYTES {
|
|
136
|
+
return Err("PDF attachment is too large (> 10 MiB)".into());
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let directory = crate::config::config_path()
|
|
140
|
+
.map_err(|error| error.to_string())?
|
|
141
|
+
.with_file_name("Attachments");
|
|
142
|
+
std::fs::create_dir_all(&directory).map_err(|error| error.to_string())?;
|
|
143
|
+
|
|
144
|
+
let safe_name: String = document
|
|
145
|
+
.filename
|
|
146
|
+
.chars()
|
|
147
|
+
.filter(|c| c.is_ascii_alphanumeric() || matches!(*c, '.' | '-' | '_'))
|
|
148
|
+
.take(80)
|
|
149
|
+
.collect();
|
|
150
|
+
let safe_name = if safe_name.is_empty() {
|
|
151
|
+
"document".into()
|
|
152
|
+
} else {
|
|
153
|
+
safe_name
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
let path = directory.join(format!(
|
|
157
|
+
"mint-pdf-{}-{}.pdf",
|
|
158
|
+
COUNTER.fetch_add(1, std::sync::atomic::Ordering::SeqCst),
|
|
159
|
+
safe_name
|
|
160
|
+
));
|
|
161
|
+
std::fs::write(&path, bytes).map_err(|error| error.to_string())?;
|
|
162
|
+
|
|
163
|
+
let extracted = crate::knowledge::extract_document_text(&path, config)
|
|
164
|
+
.map_err(|error| error.to_string());
|
|
165
|
+
let _ = std::fs::remove_file(&path);
|
|
166
|
+
let extracted = extracted?;
|
|
167
|
+
|
|
168
|
+
let mut context: String = extracted.chars().take(MAX_DOCUMENT_CONTEXT_CHARS).collect();
|
|
169
|
+
if extracted.chars().count() > MAX_DOCUMENT_CONTEXT_CHARS {
|
|
170
|
+
context.push_str("\n\n[PDF text truncated because it is long.]");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let filename = document.filename.trim();
|
|
174
|
+
let filename = if filename.is_empty() {
|
|
175
|
+
"attached.pdf"
|
|
176
|
+
} else {
|
|
177
|
+
filename
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
let mut enriched = self.clone();
|
|
181
|
+
enriched.document_attachment = None;
|
|
182
|
+
enriched.message = format!(
|
|
183
|
+
"{}\n\nAttached PDF: {filename}\nUse the extracted PDF text below when answering. If the user asks for a summary, summarize this document.\n\n--- Extracted PDF text ---\n{context}\n--- End extracted PDF text ---",
|
|
184
|
+
self.message
|
|
185
|
+
);
|
|
186
|
+
Ok(enriched)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
111
190
|
#[derive(Debug, Clone, Serialize)]
|
|
112
191
|
#[serde(rename_all = "camelCase")]
|
|
113
192
|
pub struct ChatResponse {
|
|
@@ -78,7 +78,7 @@ pub use symbols::{CodeSymbol, SymbolError, SymbolIndex, build_symbol_index};
|
|
|
78
78
|
pub use tasks::{Task, TaskError, TaskStore, tasks_path};
|
|
79
79
|
pub use tts::{TtsUrl, google_tts_urls};
|
|
80
80
|
pub use weather::{WeatherError, WeatherReport, weather};
|
|
81
|
-
pub use workflows::{WorkflowError, load_workflows, workflows_path};
|
|
81
|
+
pub use workflows::{WorkflowError, load_workflows, save_workflows, workflows_path};
|
|
82
82
|
pub mod api_server;
|
|
83
83
|
pub use api_server::start_api_server;
|
|
84
84
|
pub mod channels;
|
|
@@ -79,3 +79,11 @@ fn save_default_workflows(path: &PathBuf) -> Result<(), WorkflowError> {
|
|
|
79
79
|
source,
|
|
80
80
|
})
|
|
81
81
|
}
|
|
82
|
+
|
|
83
|
+
pub fn save_workflows(workflows: &[Value]) -> Result<(), WorkflowError> {
|
|
84
|
+
let path = workflows_path()?;
|
|
85
|
+
let directory = path.parent().ok_or(WorkflowError::MissingDirectory)?;
|
|
86
|
+
fs::create_dir_all(directory).map_err(WorkflowError::CreateDirectory)?;
|
|
87
|
+
let raw = serde_json::to_string_pretty(workflows).map_err(WorkflowError::Serialize)?;
|
|
88
|
+
fs::write(&path, format!("{raw}\n")).map_err(|source| WorkflowError::Write { path, source })
|
|
89
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pheem49/mint",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "A native Tauri desktop AI assistant with a Rust backend and React UI.",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev:web": "vite --config vite.config.web.ts",
|
|
@@ -58,6 +58,7 @@
|
|
|
58
58
|
"dependencies": {
|
|
59
59
|
"@tauri-apps/api": "^2.0.0",
|
|
60
60
|
"@vitejs/plugin-react": "^6.0.1",
|
|
61
|
+
"@xyflow/react": "^12.11.1",
|
|
61
62
|
"material-icon-theme": "5.34.0",
|
|
62
63
|
"pixi-live2d-display": "^0.4.0",
|
|
63
64
|
"pixi.js": "^6.5.10",
|
package/src/renderer/src/App.tsx
CHANGED
|
@@ -6,7 +6,6 @@ const WidgetWindow = lazy(() => import('./components/WidgetWindow'))
|
|
|
6
6
|
const ProactiveGlow = lazy(() => import('./components/ProactiveGlow'))
|
|
7
7
|
const ScreenPicker = lazy(() => import('./components/ScreenPicker'))
|
|
8
8
|
const MintDashboard = lazy(() => import('./components/MintDashboard'))
|
|
9
|
-
|
|
10
9
|
export default function App() {
|
|
11
10
|
const [hash, setHash] = useState(window.location.hash || '#/')
|
|
12
11
|
|
|
@@ -29,5 +28,6 @@ export default function App() {
|
|
|
29
28
|
if (route.startsWith('/proactive-glow')) content = <ProactiveGlow />
|
|
30
29
|
if (route.startsWith('/screen-picker')) content = <ScreenPicker />
|
|
31
30
|
|
|
31
|
+
|
|
32
32
|
return <Suspense fallback={null}>{content}</Suspense>
|
|
33
33
|
}
|
|
@@ -1702,21 +1702,33 @@ export default function ChatPanel({
|
|
|
1702
1702
|
return (
|
|
1703
1703
|
<section className={`conversation-panel ${isEmptyChat ? 'is-empty' : ''}`}>
|
|
1704
1704
|
<div className="chat-container">
|
|
1705
|
-
{interactions.map((interaction) =>
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
<
|
|
1705
|
+
{interactions.map((interaction) => {
|
|
1706
|
+
const isSystemEvent = interaction.provider === 'system' && interaction.model === 'provider_change';
|
|
1707
|
+
if (isSystemEvent) {
|
|
1708
|
+
return (
|
|
1709
|
+
<div key={interaction.id} className="system-event-divider">
|
|
1710
|
+
<div className="system-event-line" />
|
|
1711
|
+
<div className="system-event-pill">
|
|
1712
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
|
1713
|
+
<path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z" />
|
|
1714
|
+
</svg>
|
|
1715
|
+
<span>{interaction.userText}</span>
|
|
1716
1716
|
</div>
|
|
1717
|
+
<div className="system-event-line" />
|
|
1717
1718
|
</div>
|
|
1718
|
-
)
|
|
1719
|
-
|
|
1719
|
+
);
|
|
1720
|
+
}
|
|
1721
|
+
return (
|
|
1722
|
+
<div key={interaction.id} style={{ display: 'flex', flexDirection: 'column', gap: '8px', marginBottom: '16px' }}>
|
|
1723
|
+
{interaction.userText && (
|
|
1724
|
+
<div className="message user-message">
|
|
1725
|
+
<div className="bubble-wrapper">
|
|
1726
|
+
<div className="message-bubble">{renderFormattedMessage(interaction.userText)}</div>
|
|
1727
|
+
<div className="message-time"><span>{new Date(interaction.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span></div>
|
|
1728
|
+
</div>
|
|
1729
|
+
</div>
|
|
1730
|
+
)}
|
|
1731
|
+
<div className="message ai-message">
|
|
1720
1732
|
<div className="bubble-wrapper">
|
|
1721
1733
|
{renderCompletedActivity(interaction)}
|
|
1722
1734
|
{renderFileChanges(interaction)}
|
|
@@ -1750,7 +1762,8 @@ export default function ChatPanel({
|
|
|
1750
1762
|
</div>
|
|
1751
1763
|
</div>
|
|
1752
1764
|
</div>
|
|
1753
|
-
)
|
|
1765
|
+
);
|
|
1766
|
+
})}
|
|
1754
1767
|
|
|
1755
1768
|
{sending && (
|
|
1756
1769
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px', marginBottom: '16px' }}>
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { useState } from 'react'
|
|
1
|
+
import { useState, useEffect, useRef } from 'react'
|
|
2
2
|
|
|
3
|
-
export type DashboardView = 'chat' | 'pictures' | 'model' | '
|
|
3
|
+
export type DashboardView = 'chat' | 'pictures' | 'model' | 'workspace' | 'imagine' | 'workflows'
|
|
4
4
|
|
|
5
5
|
interface ChatSessionItem {
|
|
6
6
|
id: string
|
|
7
7
|
title: string
|
|
8
8
|
kind: string
|
|
9
|
+
createdAt?: string
|
|
10
|
+
updatedAt?: string
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
interface DashboardSidebarProps {
|
|
@@ -33,6 +35,8 @@ interface DashboardSidebarProps {
|
|
|
33
35
|
onSetInteractionEnabled: (enabled: boolean) => void
|
|
34
36
|
onSetShowInteractionGuide: (visible: boolean) => void
|
|
35
37
|
onShowToast: (message: string) => void
|
|
38
|
+
isSearchOpen: boolean
|
|
39
|
+
onSetSearchOpen: (open: boolean) => void
|
|
36
40
|
}
|
|
37
41
|
|
|
38
42
|
export default function DashboardSidebar({
|
|
@@ -60,9 +64,23 @@ export default function DashboardSidebar({
|
|
|
60
64
|
onSetInteractionEnabled,
|
|
61
65
|
onSetShowInteractionGuide,
|
|
62
66
|
onShowToast,
|
|
67
|
+
isSearchOpen,
|
|
68
|
+
onSetSearchOpen,
|
|
63
69
|
}: DashboardSidebarProps) {
|
|
64
70
|
const [editingSessionId, setEditingSessionId] = useState<string | null>(null)
|
|
65
71
|
const [editTitleValue, setEditTitleValue] = useState('')
|
|
72
|
+
const [isMoreOpen, setIsMoreOpen] = useState(false)
|
|
73
|
+
const moreContainerRef = useRef<HTMLDivElement>(null)
|
|
74
|
+
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
function handleClickOutside(event: MouseEvent) {
|
|
77
|
+
if (moreContainerRef.current && !moreContainerRef.current.contains(event.target as Node)) {
|
|
78
|
+
setIsMoreOpen(false)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
document.addEventListener('mousedown', handleClickOutside)
|
|
82
|
+
return () => document.removeEventListener('mousedown', handleClickOutside)
|
|
83
|
+
}, [])
|
|
66
84
|
|
|
67
85
|
const handleSaveRename = (id: string) => {
|
|
68
86
|
if (editTitleValue.trim() && editTitleValue.trim() !== chatSessions.find(s => s.id === id)?.title) {
|
|
@@ -115,6 +133,16 @@ export default function DashboardSidebar({
|
|
|
115
133
|
<span>New Chat</span>
|
|
116
134
|
</button>
|
|
117
135
|
|
|
136
|
+
<button className="sidebar-top-action sidebar-search-btn" onClick={() => onSetSearchOpen(true)}>
|
|
137
|
+
<span aria-hidden="true" style={{ display: 'inline-flex', alignItems: 'center' }}>
|
|
138
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
|
139
|
+
<circle cx="11" cy="11" r="8"></circle>
|
|
140
|
+
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
|
141
|
+
</svg>
|
|
142
|
+
</span>
|
|
143
|
+
<span>Search Chats</span>
|
|
144
|
+
</button>
|
|
145
|
+
|
|
118
146
|
<button className={`sidebar-top-action ${view === 'chat' ? 'is-active' : ''}`} onClick={() => onSetView('chat')}>
|
|
119
147
|
<span aria-hidden="true" style={{ display: 'inline-flex', alignItems: 'center' }}>
|
|
120
148
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
|
@@ -133,22 +161,14 @@ export default function DashboardSidebar({
|
|
|
133
161
|
</span>
|
|
134
162
|
<span>Pictures</span>
|
|
135
163
|
</button>
|
|
136
|
-
<button className={`sidebar-top-action ${view === '
|
|
137
|
-
<span aria-hidden="true" style={{ display: 'inline-flex', alignItems: 'center' }}>
|
|
138
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
|
139
|
-
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>
|
|
140
|
-
</svg>
|
|
141
|
-
</span>
|
|
142
|
-
<span>Image Studio</span>
|
|
143
|
-
</button>
|
|
144
|
-
<button className={`sidebar-top-action ${view === 'workspeac' ? 'is-active' : ''}`} onClick={() => onSetView('workspeac')}>
|
|
164
|
+
<button className={`sidebar-top-action ${view === 'workspace' ? 'is-active' : ''}`} onClick={() => onSetView('workspace')}>
|
|
145
165
|
<span aria-hidden="true" style={{ display: 'inline-flex', alignItems: 'center' }}>
|
|
146
166
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
|
147
167
|
<path d="M3 6h7l2 2h9v10a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z"></path>
|
|
148
168
|
<path d="M3 6v12"></path>
|
|
149
169
|
</svg>
|
|
150
170
|
</span>
|
|
151
|
-
<span>
|
|
171
|
+
<span>Workspace</span>
|
|
152
172
|
</button>
|
|
153
173
|
<button className={`sidebar-top-action ${modelVisible ? 'is-active' : ''}`} onClick={onToggleModel}>
|
|
154
174
|
<span aria-hidden="true" style={{ display: 'inline-flex', alignItems: 'center' }}>
|
|
@@ -160,6 +180,45 @@ export default function DashboardSidebar({
|
|
|
160
180
|
<span>Live2D Model</span>
|
|
161
181
|
</button>
|
|
162
182
|
|
|
183
|
+
<div className="sidebar-more-container" ref={moreContainerRef}>
|
|
184
|
+
<button className={`sidebar-top-action ${isMoreOpen || view === 'imagine' || view === 'workflows' ? 'is-active' : ''}`} onClick={() => setIsMoreOpen(!isMoreOpen)}>
|
|
185
|
+
<span aria-hidden="true" style={{ display: 'inline-flex', alignItems: 'center' }}>
|
|
186
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
|
187
|
+
<circle cx="12" cy="12" r="1.5"></circle>
|
|
188
|
+
<circle cx="19" cy="12" r="1.5"></circle>
|
|
189
|
+
<circle cx="5" cy="12" r="1.5"></circle>
|
|
190
|
+
</svg>
|
|
191
|
+
</span>
|
|
192
|
+
<span>More</span>
|
|
193
|
+
</button>
|
|
194
|
+
{isMoreOpen && (
|
|
195
|
+
<div className="sidebar-more-popover">
|
|
196
|
+
<button className={`popover-item ${view === 'imagine' ? 'active' : ''}`} onClick={() => { onSetView('imagine'); setIsMoreOpen(false); }}>
|
|
197
|
+
<span aria-hidden="true" style={{ display: 'inline-flex', alignItems: 'center' }}>
|
|
198
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
|
199
|
+
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>
|
|
200
|
+
</svg>
|
|
201
|
+
</span>
|
|
202
|
+
<span>Image Studio</span>
|
|
203
|
+
</button>
|
|
204
|
+
<button className={`popover-item ${view === 'workflows' ? 'active' : ''}`} onClick={() => { onSetView('workflows'); setIsMoreOpen(false); }}>
|
|
205
|
+
<span aria-hidden="true" style={{ display: 'inline-flex', alignItems: 'center' }}>
|
|
206
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
|
207
|
+
<path d="M4 12h16"></path>
|
|
208
|
+
<path d="M12 4v16"></path>
|
|
209
|
+
<rect x="2" y="9" width="4" height="6" rx="1"></rect>
|
|
210
|
+
<rect x="18" y="9" width="4" height="6" rx="1"></rect>
|
|
211
|
+
<rect x="10" y="2" width="4" height="4" rx="1"></rect>
|
|
212
|
+
<rect x="10" y="18" width="4" height="4" rx="1"></rect>
|
|
213
|
+
</svg>
|
|
214
|
+
</span>
|
|
215
|
+
<span>Workflow (Beta)</span>
|
|
216
|
+
</button>
|
|
217
|
+
</div>
|
|
218
|
+
)}
|
|
219
|
+
</div>
|
|
220
|
+
|
|
221
|
+
|
|
163
222
|
{modelVisible && (
|
|
164
223
|
<div className="sidebar-model-controls">
|
|
165
224
|
<button
|
|
@@ -193,12 +193,16 @@ export default function ImageStudioPanel({ view, onRefreshPictures, onSendToChat
|
|
|
193
193
|
document.body.removeChild(a)
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
if (view !== 'imagine') return null
|
|
197
|
-
|
|
198
196
|
const modelOptions = PROVIDER_MODELS[selectedProvider] ?? []
|
|
199
197
|
|
|
200
198
|
return (
|
|
201
|
-
<div
|
|
199
|
+
<div
|
|
200
|
+
className={`img-studio ${view === 'imagine' ? 'is-visible' : ''}`}
|
|
201
|
+
id="image-studio-panel"
|
|
202
|
+
role="main"
|
|
203
|
+
aria-label="Image Studio"
|
|
204
|
+
aria-hidden={view !== 'imagine'}
|
|
205
|
+
>
|
|
202
206
|
{/* Header */}
|
|
203
207
|
<header className="img-studio-header">
|
|
204
208
|
<div className="img-studio-header-title">
|