generated from nhcarrigan/template
feat: add quick actions panel for common prompts
- Add Rust backend for managing quick actions with persistent storage - Create QuickActionsPanel component with edit/delete functionality - Add quickActions store for frontend state management - Move Actions and Snippets buttons to input controls row - Include 6 default quick actions: Review PR, Run Tests, Explain File, Fix Error, Write Tests, and Refactor - Support custom quick action creation and management Closes #15
This commit is contained in:
@@ -4,6 +4,7 @@ mod commands;
|
|||||||
mod config;
|
mod config;
|
||||||
mod git;
|
mod git;
|
||||||
mod notifications;
|
mod notifications;
|
||||||
|
mod quick_actions;
|
||||||
mod sessions;
|
mod sessions;
|
||||||
mod snippets;
|
mod snippets;
|
||||||
mod stats;
|
mod stats;
|
||||||
@@ -20,6 +21,7 @@ use commands::load_saved_achievements;
|
|||||||
use commands::*;
|
use commands::*;
|
||||||
use git::*;
|
use git::*;
|
||||||
use notifications::*;
|
use notifications::*;
|
||||||
|
use quick_actions::*;
|
||||||
use sessions::*;
|
use sessions::*;
|
||||||
use snippets::*;
|
use snippets::*;
|
||||||
use tauri::Manager;
|
use tauri::Manager;
|
||||||
@@ -120,6 +122,10 @@ pub fn run() {
|
|||||||
delete_snippet,
|
delete_snippet,
|
||||||
get_snippet_categories,
|
get_snippet_categories,
|
||||||
reset_default_snippets,
|
reset_default_snippets,
|
||||||
|
list_quick_actions,
|
||||||
|
save_quick_action,
|
||||||
|
delete_quick_action,
|
||||||
|
reset_default_quick_actions,
|
||||||
git_status,
|
git_status,
|
||||||
git_diff,
|
git_diff,
|
||||||
git_branches,
|
git_branches,
|
||||||
|
|||||||
@@ -0,0 +1,191 @@
|
|||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tauri::AppHandle;
|
||||||
|
use tauri_plugin_store::StoreExt;
|
||||||
|
|
||||||
|
const QUICK_ACTIONS_STORE_KEY: &str = "quick_actions";
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct QuickAction {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub prompt: String,
|
||||||
|
pub icon: String,
|
||||||
|
pub is_default: bool,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub updated_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_default_quick_actions() -> Vec<QuickAction> {
|
||||||
|
let now = Utc::now();
|
||||||
|
vec![
|
||||||
|
QuickAction {
|
||||||
|
id: "default-review-pr".to_string(),
|
||||||
|
name: "Review PR".to_string(),
|
||||||
|
prompt: "Please review this pull request and provide feedback on code quality, potential issues, and suggestions for improvement.".to_string(),
|
||||||
|
icon: "git-pull-request".to_string(),
|
||||||
|
is_default: true,
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
},
|
||||||
|
QuickAction {
|
||||||
|
id: "default-run-tests".to_string(),
|
||||||
|
name: "Run Tests".to_string(),
|
||||||
|
prompt: "Please run the test suite for this project and report any failures or issues.".to_string(),
|
||||||
|
icon: "play".to_string(),
|
||||||
|
is_default: true,
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
},
|
||||||
|
QuickAction {
|
||||||
|
id: "default-explain-file".to_string(),
|
||||||
|
name: "Explain File".to_string(),
|
||||||
|
prompt: "Please explain what this file does, its purpose, and how it fits into the overall project structure.".to_string(),
|
||||||
|
icon: "file-text".to_string(),
|
||||||
|
is_default: true,
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
},
|
||||||
|
QuickAction {
|
||||||
|
id: "default-fix-error".to_string(),
|
||||||
|
name: "Fix Error".to_string(),
|
||||||
|
prompt: "I'm getting an error. Can you help me identify the cause and fix it?".to_string(),
|
||||||
|
icon: "alert-circle".to_string(),
|
||||||
|
is_default: true,
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
},
|
||||||
|
QuickAction {
|
||||||
|
id: "default-write-tests".to_string(),
|
||||||
|
name: "Write Tests".to_string(),
|
||||||
|
prompt: "Please write comprehensive unit tests for the current code with good coverage.".to_string(),
|
||||||
|
icon: "check-square".to_string(),
|
||||||
|
is_default: true,
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
},
|
||||||
|
QuickAction {
|
||||||
|
id: "default-refactor".to_string(),
|
||||||
|
name: "Refactor".to_string(),
|
||||||
|
prompt: "Please refactor this code to improve readability, maintainability, and performance.".to_string(),
|
||||||
|
icon: "refresh-cw".to_string(),
|
||||||
|
is_default: true,
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_all_quick_actions(app: &AppHandle) -> Result<Vec<QuickAction>, String> {
|
||||||
|
let store = app
|
||||||
|
.store("hikari-quick-actions.json")
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
match store.get(QUICK_ACTIONS_STORE_KEY) {
|
||||||
|
Some(value) => {
|
||||||
|
let mut actions: Vec<QuickAction> =
|
||||||
|
serde_json::from_value(value.clone()).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
let defaults = get_default_quick_actions();
|
||||||
|
for default in defaults {
|
||||||
|
if !actions.iter().any(|a| a.id == default.id) {
|
||||||
|
actions.push(default);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(actions)
|
||||||
|
}
|
||||||
|
None => Ok(get_default_quick_actions()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_all_quick_actions(app: &AppHandle, actions: &[QuickAction]) -> Result<(), String> {
|
||||||
|
let store = app
|
||||||
|
.store("hikari-quick-actions.json")
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
let value = serde_json::to_value(actions).map_err(|e| e.to_string())?;
|
||||||
|
store.set(QUICK_ACTIONS_STORE_KEY, value);
|
||||||
|
store.save().map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn list_quick_actions(app: AppHandle) -> Result<Vec<QuickAction>, String> {
|
||||||
|
let mut actions = load_all_quick_actions(&app)?;
|
||||||
|
|
||||||
|
actions.sort_by(|a, b| {
|
||||||
|
let default_cmp = b.is_default.cmp(&a.is_default);
|
||||||
|
if default_cmp == std::cmp::Ordering::Equal {
|
||||||
|
a.name.cmp(&b.name)
|
||||||
|
} else {
|
||||||
|
default_cmp
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(actions)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn save_quick_action(app: AppHandle, action: QuickAction) -> Result<(), String> {
|
||||||
|
let mut actions = load_all_quick_actions(&app)?;
|
||||||
|
|
||||||
|
if let Some(existing) = actions.iter_mut().find(|a| a.id == action.id) {
|
||||||
|
let mut updated = action;
|
||||||
|
updated.is_default = existing.is_default;
|
||||||
|
*existing = updated;
|
||||||
|
} else {
|
||||||
|
actions.push(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
save_all_quick_actions(&app, &actions)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn delete_quick_action(app: AppHandle, action_id: String) -> Result<(), String> {
|
||||||
|
let mut actions = load_all_quick_actions(&app)?;
|
||||||
|
|
||||||
|
if actions
|
||||||
|
.iter()
|
||||||
|
.any(|a| a.id == action_id && a.is_default)
|
||||||
|
{
|
||||||
|
return Err("Cannot delete default quick actions".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
actions.retain(|a| a.id != action_id);
|
||||||
|
save_all_quick_actions(&app, &actions)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn reset_default_quick_actions(app: AppHandle) -> Result<(), String> {
|
||||||
|
let mut actions = load_all_quick_actions(&app)?;
|
||||||
|
|
||||||
|
actions.retain(|a| !a.is_default);
|
||||||
|
actions.extend(get_default_quick_actions());
|
||||||
|
|
||||||
|
save_all_quick_actions(&app, &actions)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_default_quick_actions_exist() {
|
||||||
|
let defaults = get_default_quick_actions();
|
||||||
|
assert!(!defaults.is_empty());
|
||||||
|
assert!(defaults.iter().all(|a| a.is_default));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_default_quick_actions_have_required_fields() {
|
||||||
|
let defaults = get_default_quick_actions();
|
||||||
|
for action in defaults {
|
||||||
|
assert!(!action.id.is_empty());
|
||||||
|
assert!(!action.name.is_empty());
|
||||||
|
assert!(!action.prompt.is_empty());
|
||||||
|
assert!(!action.icon.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,6 +26,7 @@
|
|||||||
} from "$lib/commands/slashCommands";
|
} from "$lib/commands/slashCommands";
|
||||||
import AttachmentPreview from "$lib/components/AttachmentPreview.svelte";
|
import AttachmentPreview from "$lib/components/AttachmentPreview.svelte";
|
||||||
import SnippetLibraryPanel from "$lib/components/SnippetLibraryPanel.svelte";
|
import SnippetLibraryPanel from "$lib/components/SnippetLibraryPanel.svelte";
|
||||||
|
import QuickActionsPanel from "$lib/components/QuickActionsPanel.svelte";
|
||||||
import type { Attachment } from "$lib/types/messages";
|
import type { Attachment } from "$lib/types/messages";
|
||||||
|
|
||||||
const INPUT_HISTORY_KEY = "hikari-input-history";
|
const INPUT_HISTORY_KEY = "hikari-input-history";
|
||||||
@@ -41,6 +42,7 @@
|
|||||||
let attachments = $state<Attachment[]>([]);
|
let attachments = $state<Attachment[]>([]);
|
||||||
let isDragging = $state(false);
|
let isDragging = $state(false);
|
||||||
let showSnippetLibrary = $state(false);
|
let showSnippetLibrary = $state(false);
|
||||||
|
let showQuickActions = $state(false);
|
||||||
|
|
||||||
// Input history state
|
// Input history state
|
||||||
let inputHistory = $state<string[]>([]);
|
let inputHistory = $state<string[]>([]);
|
||||||
@@ -629,6 +631,42 @@ User: ${formattedMessage}`;
|
|||||||
userHasTyped = true;
|
userHasTyped = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleQuickAction(prompt: string): Promise<void> {
|
||||||
|
// Quick actions send the prompt directly
|
||||||
|
if (!isConnected || isSubmitting) return;
|
||||||
|
|
||||||
|
// Add to history
|
||||||
|
addToHistory(prompt);
|
||||||
|
historyIndex = -1;
|
||||||
|
tempInput = "";
|
||||||
|
userHasTyped = false;
|
||||||
|
|
||||||
|
isSubmitting = true;
|
||||||
|
|
||||||
|
// Reset notification state for new user message
|
||||||
|
handleNewUserMessage();
|
||||||
|
|
||||||
|
claudeStore.addLine("user", prompt);
|
||||||
|
characterState.setState("thinking");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const conversationId = get(claudeStore.activeConversationId);
|
||||||
|
if (!conversationId) {
|
||||||
|
throw new Error("No active conversation");
|
||||||
|
}
|
||||||
|
await invoke("send_prompt", {
|
||||||
|
conversationId,
|
||||||
|
message: prompt,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to send quick action:", error);
|
||||||
|
claudeStore.addLine("error", `Failed to send: ${error}`);
|
||||||
|
characterState.setTemporaryState("error", 3000);
|
||||||
|
} finally {
|
||||||
|
isSubmitting = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleKeyDown(event: KeyboardEvent) {
|
function handleKeyDown(event: KeyboardEvent) {
|
||||||
// Handle command menu navigation
|
// Handle command menu navigation
|
||||||
if (showCommandMenu && matchingCommands.length > 0) {
|
if (showCommandMenu && matchingCommands.length > 0) {
|
||||||
@@ -705,6 +743,50 @@ User: ${formattedMessage}`;
|
|||||||
|
|
||||||
<div class="input-controls flex gap-2 mb-2">
|
<div class="input-controls flex gap-2 mb-2">
|
||||||
<MessageModeSelector />
|
<MessageModeSelector />
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={() => (showQuickActions = true)}
|
||||||
|
class="control-button"
|
||||||
|
title="Quick Actions"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||||
|
</svg>
|
||||||
|
<span>Actions</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={() => (showSnippetLibrary = true)}
|
||||||
|
class="control-button"
|
||||||
|
title="Snippet Library"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z" />
|
||||||
|
<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" />
|
||||||
|
<line x1="10" y1="9" x2="8" y2="9" />
|
||||||
|
</svg>
|
||||||
|
<span>Snippets</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="input-row">
|
<div class="input-row">
|
||||||
@@ -735,29 +817,6 @@ User: ${formattedMessage}`;
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="button-wrapper">
|
<div class="button-wrapper">
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onclick={() => (showSnippetLibrary = true)}
|
|
||||||
class="attach-button"
|
|
||||||
title="Snippet Library"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
width="20"
|
|
||||||
height="20"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
>
|
|
||||||
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z" />
|
|
||||||
<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" />
|
|
||||||
<line x1="10" y1="9" x2="8" y2="9" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<button type="button" onclick={handleFilePicker} class="attach-button" title="Attach files">
|
<button type="button" onclick={handleFilePicker} class="attach-button" title="Attach files">
|
||||||
<svg
|
<svg
|
||||||
width="20"
|
width="20"
|
||||||
@@ -811,6 +870,13 @@ User: ${formattedMessage}`;
|
|||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if showQuickActions}
|
||||||
|
<QuickActionsPanel
|
||||||
|
onClose={() => (showQuickActions = false)}
|
||||||
|
onAction={handleQuickAction}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.input-bar {
|
.input-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -853,6 +919,32 @@ User: ${formattedMessage}`;
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.control-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
font-size: 14px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-button:hover {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border-color: var(--accent-primary);
|
||||||
|
color: var(--accent-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-button:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
.input-row {
|
.input-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
|||||||
@@ -0,0 +1,451 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import { quickActionsStore, type QuickAction } from "$lib/stores/quickActions";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onClose: () => void;
|
||||||
|
onAction: (prompt: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { onClose, onAction }: Props = $props();
|
||||||
|
|
||||||
|
let editingAction = $state<QuickAction | null>(null);
|
||||||
|
let isCreating = $state(false);
|
||||||
|
let showDeleteConfirm = $state<string | null>(null);
|
||||||
|
|
||||||
|
let editName = $state("");
|
||||||
|
let editPrompt = $state("");
|
||||||
|
let editIcon = $state("zap");
|
||||||
|
|
||||||
|
const actions = $derived(quickActionsStore.actions);
|
||||||
|
const isLoading = $derived(quickActionsStore.isLoading);
|
||||||
|
|
||||||
|
const availableIcons = [
|
||||||
|
{ id: "zap", label: "Lightning" },
|
||||||
|
{ id: "play", label: "Play" },
|
||||||
|
{ id: "file-text", label: "File" },
|
||||||
|
{ id: "alert-circle", label: "Alert" },
|
||||||
|
{ id: "check-square", label: "Check" },
|
||||||
|
{ id: "refresh-cw", label: "Refresh" },
|
||||||
|
{ id: "git-pull-request", label: "Git PR" },
|
||||||
|
{ id: "code", label: "Code" },
|
||||||
|
{ id: "search", label: "Search" },
|
||||||
|
{ id: "terminal", label: "Terminal" },
|
||||||
|
{ id: "bug", label: "Bug" },
|
||||||
|
{ id: "shield", label: "Shield" },
|
||||||
|
];
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
quickActionsStore.loadQuickActions();
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleAction(action: QuickAction): void {
|
||||||
|
onAction(action.prompt);
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleStartCreate(): void {
|
||||||
|
isCreating = true;
|
||||||
|
editingAction = null;
|
||||||
|
editName = "";
|
||||||
|
editPrompt = "";
|
||||||
|
editIcon = "zap";
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleStartEdit(action: QuickAction): void {
|
||||||
|
editingAction = action;
|
||||||
|
isCreating = false;
|
||||||
|
editName = action.name;
|
||||||
|
editPrompt = action.prompt;
|
||||||
|
editIcon = action.icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCancelEdit(): void {
|
||||||
|
editingAction = null;
|
||||||
|
isCreating = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSave(): Promise<void> {
|
||||||
|
if (!editName.trim() || !editPrompt.trim()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCreating) {
|
||||||
|
await quickActionsStore.createQuickAction(editName.trim(), editPrompt.trim(), editIcon);
|
||||||
|
} else if (editingAction) {
|
||||||
|
await quickActionsStore.updateQuickAction(
|
||||||
|
editingAction.id,
|
||||||
|
editName.trim(),
|
||||||
|
editPrompt.trim(),
|
||||||
|
editIcon
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCancelEdit();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDelete(actionId: string): Promise<void> {
|
||||||
|
await quickActionsStore.deleteQuickAction(actionId);
|
||||||
|
showDeleteConfirm = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIconSvg(icon: string): string {
|
||||||
|
const icons: Record<string, string> = {
|
||||||
|
"git-pull-request":
|
||||||
|
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 3v12M18 9a3 3 0 100 6 3 3 0 000-6zm0 0V3m0 18v-6M6 21a3 3 0 100-6 3 3 0 000 6z" />',
|
||||||
|
play: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />',
|
||||||
|
"file-text":
|
||||||
|
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />',
|
||||||
|
"alert-circle":
|
||||||
|
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />',
|
||||||
|
"check-square":
|
||||||
|
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" />',
|
||||||
|
"refresh-cw":
|
||||||
|
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />',
|
||||||
|
zap: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />',
|
||||||
|
code: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />',
|
||||||
|
search:
|
||||||
|
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />',
|
||||||
|
terminal:
|
||||||
|
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />',
|
||||||
|
bug: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />',
|
||||||
|
shield:
|
||||||
|
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />',
|
||||||
|
};
|
||||||
|
return icons[icon] || icons["zap"];
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4"
|
||||||
|
onclick={onClose}
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
onkeydown={(e) => e.key === "Escape" && onClose()}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="bg-[var(--bg-primary)] border border-[var(--border-color)] rounded-lg shadow-xl max-w-2xl w-full max-h-[80vh] overflow-hidden flex flex-col"
|
||||||
|
onclick={(e) => e.stopPropagation()}
|
||||||
|
onkeydown={(e) => e.stopPropagation()}
|
||||||
|
role="dialog"
|
||||||
|
aria-labelledby="quick-actions-title"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between p-6 pb-4 border-b border-[var(--border-color)]">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
{#if editingAction || isCreating}
|
||||||
|
<button
|
||||||
|
onclick={handleCancelEdit}
|
||||||
|
class="p-1 text-[var(--text-secondary)] hover:text-[var(--text-primary)] transition-colors"
|
||||||
|
aria-label="Back to list"
|
||||||
|
>
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M15 19l-7-7 7-7"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
<h2 id="quick-actions-title" class="text-xl font-semibold text-[var(--text-primary)]">
|
||||||
|
{#if isCreating}
|
||||||
|
Create Quick Action
|
||||||
|
{:else if editingAction}
|
||||||
|
Edit Quick Action
|
||||||
|
{:else}
|
||||||
|
Quick Actions
|
||||||
|
{/if}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
{#if !editingAction && !isCreating}
|
||||||
|
<button
|
||||||
|
onclick={handleStartCreate}
|
||||||
|
class="px-3 py-1.5 text-sm font-medium bg-[var(--accent-primary)] text-white rounded hover:bg-[var(--accent-primary)]/80 transition-colors flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M12 4v16m8-8H4"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
New Action
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
<button
|
||||||
|
onclick={onClose}
|
||||||
|
class="p-1 text-[var(--text-secondary)] hover:text-[var(--text-primary)] transition-colors"
|
||||||
|
aria-label="Close"
|
||||||
|
>
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M6 18L18 6M6 6l12 12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if editingAction || isCreating}
|
||||||
|
<div class="p-6 flex-1 overflow-y-auto">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
for="action-name"
|
||||||
|
class="block text-sm font-medium text-[var(--text-secondary)] mb-1"
|
||||||
|
>
|
||||||
|
Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="action-name"
|
||||||
|
type="text"
|
||||||
|
bind:value={editName}
|
||||||
|
placeholder="Enter action name..."
|
||||||
|
class="w-full px-4 py-2 bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-lg text-[var(--text-primary)] placeholder-[var(--text-tertiary)] focus:outline-none focus:border-[var(--accent-primary)]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
for="action-icon"
|
||||||
|
class="block text-sm font-medium text-[var(--text-secondary)] mb-1"
|
||||||
|
>
|
||||||
|
Icon
|
||||||
|
</label>
|
||||||
|
<div class="grid grid-cols-6 gap-2">
|
||||||
|
{#each availableIcons as icon (icon.id)}
|
||||||
|
<button
|
||||||
|
onclick={() => (editIcon = icon.id)}
|
||||||
|
class="p-3 rounded-lg border transition-colors {editIcon === icon.id
|
||||||
|
? 'bg-[var(--accent-primary)]/10 border-[var(--accent-primary)] text-[var(--accent-primary)]'
|
||||||
|
: 'bg-[var(--bg-secondary)] border-[var(--border-color)] text-[var(--text-secondary)] hover:border-[var(--accent-primary)]/50'}"
|
||||||
|
title={icon.label}
|
||||||
|
>
|
||||||
|
<svg class="w-5 h-5 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
{@html getIconSvg(icon.id)}
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
for="action-prompt"
|
||||||
|
class="block text-sm font-medium text-[var(--text-secondary)] mb-1"
|
||||||
|
>
|
||||||
|
Prompt
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="action-prompt"
|
||||||
|
bind:value={editPrompt}
|
||||||
|
placeholder="Enter the prompt to send..."
|
||||||
|
rows="4"
|
||||||
|
class="w-full px-4 py-3 bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-lg text-[var(--text-primary)] placeholder-[var(--text-tertiary)] focus:outline-none focus:border-[var(--accent-primary)] resize-none font-mono text-sm"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end gap-2 pt-4">
|
||||||
|
<button
|
||||||
|
onclick={handleCancelEdit}
|
||||||
|
class="px-4 py-2 text-sm font-medium bg-[var(--bg-secondary)] text-[var(--text-secondary)] rounded-lg hover:bg-[var(--bg-tertiary)] transition-colors"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onclick={handleSave}
|
||||||
|
disabled={!editName.trim() || !editPrompt.trim()}
|
||||||
|
class="px-4 py-2 text-sm font-medium bg-[var(--accent-primary)] text-white rounded-lg hover:bg-[var(--accent-primary)]/80 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{isCreating ? "Create" : "Save"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="flex-1 overflow-y-auto p-6">
|
||||||
|
{#if $isLoading}
|
||||||
|
<div class="flex items-center justify-center p-8">
|
||||||
|
<div class="text-[var(--text-tertiary)]">Loading quick actions...</div>
|
||||||
|
</div>
|
||||||
|
{:else if $actions.length === 0}
|
||||||
|
<div class="flex flex-col items-center justify-center p-8 text-center">
|
||||||
|
<svg
|
||||||
|
class="w-16 h-16 text-[var(--text-tertiary)] mb-4"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="1.5"
|
||||||
|
d="M13 10V3L4 14h7v7l9-11h-7z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<p class="text-[var(--text-secondary)]">No quick actions available</p>
|
||||||
|
<button
|
||||||
|
onclick={handleStartCreate}
|
||||||
|
class="mt-4 px-4 py-2 text-sm font-medium bg-[var(--accent-primary)] text-white rounded-lg hover:bg-[var(--accent-primary)]/80 transition-colors"
|
||||||
|
>
|
||||||
|
Create your first action
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3">
|
||||||
|
{#each $actions as action (action.id)}
|
||||||
|
<div
|
||||||
|
class="group relative bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-lg hover:border-[var(--accent-primary)]/50 transition-colors"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onclick={() => handleAction(action)}
|
||||||
|
class="w-full p-4 text-left"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-3 mb-2">
|
||||||
|
<div
|
||||||
|
class="w-10 h-10 rounded-lg bg-[var(--accent-primary)]/10 flex items-center justify-center text-[var(--accent-primary)]"
|
||||||
|
>
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
{@html getIconSvg(action.icon)}
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<h3 class="font-medium text-[var(--text-primary)] truncate">{action.name}</h3>
|
||||||
|
{#if action.is_default}
|
||||||
|
<span class="text-xs text-[var(--text-tertiary)]">Default</span>
|
||||||
|
{:else}
|
||||||
|
<span class="text-xs text-[var(--accent-secondary)]">Custom</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-[var(--text-tertiary)] line-clamp-2">{action.prompt}</p>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
class="absolute top-2 right-2 flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onclick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleStartEdit(action);
|
||||||
|
}}
|
||||||
|
class="p-1.5 text-[var(--text-tertiary)] hover:text-[var(--text-primary)] bg-[var(--bg-primary)] rounded transition-colors"
|
||||||
|
title="Edit action"
|
||||||
|
>
|
||||||
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{#if !action.is_default}
|
||||||
|
{#if showDeleteConfirm === action.id}
|
||||||
|
<div class="flex items-center gap-1 bg-[var(--bg-primary)] rounded p-1">
|
||||||
|
<button
|
||||||
|
onclick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleDelete(action.id);
|
||||||
|
}}
|
||||||
|
class="px-2 py-0.5 text-xs font-medium bg-red-500 text-white rounded hover:bg-red-600 transition-colors"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onclick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
showDeleteConfirm = null;
|
||||||
|
}}
|
||||||
|
class="px-2 py-0.5 text-xs font-medium bg-[var(--bg-tertiary)] text-[var(--text-secondary)] rounded hover:bg-[var(--bg-secondary)] transition-colors"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
onclick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
showDeleteConfirm = action.id;
|
||||||
|
}}
|
||||||
|
class="p-1.5 text-[var(--text-tertiary)] hover:text-red-400 bg-[var(--bg-primary)] rounded transition-colors"
|
||||||
|
title="Delete action"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-3.5 h-3.5"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
[role="dialog"] {
|
||||||
|
animation: slideIn 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.95) translateY(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1) translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.overflow-y-auto {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: var(--border-color) transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overflow-y-auto::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overflow-y-auto::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overflow-y-auto::-webkit-scrollbar-thumb {
|
||||||
|
background-color: var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overflow-y-auto::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: var(--accent-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-clamp-2 {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import { writable } from "svelte/store";
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
|
||||||
|
export interface QuickAction {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
prompt: string;
|
||||||
|
icon: string;
|
||||||
|
is_default: boolean;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createQuickActionsStore() {
|
||||||
|
const actions = writable<QuickAction[]>([]);
|
||||||
|
const isLoading = writable(false);
|
||||||
|
|
||||||
|
async function loadQuickActions(): Promise<void> {
|
||||||
|
isLoading.set(true);
|
||||||
|
try {
|
||||||
|
const actionList = await invoke<QuickAction[]>("list_quick_actions");
|
||||||
|
actions.set(actionList);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load quick actions:", error);
|
||||||
|
} finally {
|
||||||
|
isLoading.set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveQuickAction(action: QuickAction): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await invoke("save_quick_action", { action });
|
||||||
|
await loadQuickActions();
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to save quick action:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createQuickAction(
|
||||||
|
name: string,
|
||||||
|
prompt: string,
|
||||||
|
icon: string
|
||||||
|
): Promise<boolean> {
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const action: QuickAction = {
|
||||||
|
id: `custom-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
|
||||||
|
name,
|
||||||
|
prompt,
|
||||||
|
icon,
|
||||||
|
is_default: false,
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
};
|
||||||
|
return saveQuickAction(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateQuickAction(
|
||||||
|
id: string,
|
||||||
|
name: string,
|
||||||
|
prompt: string,
|
||||||
|
icon: string
|
||||||
|
): Promise<boolean> {
|
||||||
|
const currentActions = await invoke<QuickAction[]>("list_quick_actions");
|
||||||
|
const existing = currentActions.find((a) => a.id === id);
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
console.error("Quick action not found for update");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated: QuickAction = {
|
||||||
|
...existing,
|
||||||
|
name,
|
||||||
|
prompt,
|
||||||
|
icon,
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
return saveQuickAction(updated);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteQuickAction(actionId: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await invoke("delete_quick_action", { actionId });
|
||||||
|
await loadQuickActions();
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to delete quick action:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resetDefaults(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await invoke("reset_default_quick_actions");
|
||||||
|
await loadQuickActions();
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to reset default quick actions:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
actions: { subscribe: actions.subscribe },
|
||||||
|
isLoading: { subscribe: isLoading.subscribe },
|
||||||
|
loadQuickActions,
|
||||||
|
saveQuickAction,
|
||||||
|
createQuickAction,
|
||||||
|
updateQuickAction,
|
||||||
|
deleteQuickAction,
|
||||||
|
resetDefaults,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const quickActionsStore = createQuickActionsStore();
|
||||||
Reference in New Issue
Block a user