Last round of prettier: npx prettier --write src/
This commit is contained in:
@@ -1,125 +1,140 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* AE_AITools.svelte
|
||||
* GENERIC Aether AI Toolset (Runes/Svelte 5)
|
||||
* Extracted logic from Journals module to be system-wide.
|
||||
*/
|
||||
import OpenAI from 'openai';
|
||||
import { Modal } from 'flowbite-svelte';
|
||||
import {
|
||||
Bot, BotMessageSquare, Loader, FileText,
|
||||
Save, FilePenLine, RotateCcw, Settings,
|
||||
RefreshCcw, Globe, Copy
|
||||
} from '@lucide/svelte';
|
||||
import { ae_loc, ae_api } from '$lib/stores/ae_stores';
|
||||
import AE_Comp_Editor_CodeMirror from '$lib/elements/element_editor_codemirror.svelte';
|
||||
/**
|
||||
* AE_AITools.svelte
|
||||
* GENERIC Aether AI Toolset (Runes/Svelte 5)
|
||||
* Extracted logic from Journals module to be system-wide.
|
||||
*/
|
||||
import OpenAI from 'openai';
|
||||
import { Modal } from 'flowbite-svelte';
|
||||
import {
|
||||
Bot,
|
||||
BotMessageSquare,
|
||||
Loader,
|
||||
FileText,
|
||||
Save,
|
||||
FilePenLine,
|
||||
RotateCcw,
|
||||
Settings,
|
||||
RefreshCcw,
|
||||
Globe,
|
||||
Copy
|
||||
} from '@lucide/svelte';
|
||||
import { ae_loc, ae_api } from '$lib/stores/ae_stores';
|
||||
import AE_Comp_Editor_CodeMirror from '$lib/elements/element_editor_codemirror.svelte';
|
||||
|
||||
interface Props {
|
||||
// Core Props
|
||||
content: string; // The text to summarize/analyze
|
||||
summary: string; // The result (bindable)
|
||||
|
||||
// Configuration (Bindable for global settings persistence)
|
||||
model?: string;
|
||||
baseUrl?: string;
|
||||
token?: string;
|
||||
systemPrompt?: string;
|
||||
maxTokens?: number;
|
||||
temperature?: number;
|
||||
|
||||
// Callbacks
|
||||
onSave?: (newSummary: string) => void;
|
||||
onSyncConfig?: () => void; // Optional: callback to sync from global site config
|
||||
|
||||
// UI Customization
|
||||
buttonClass?: string;
|
||||
log_lvl?: number;
|
||||
interface Props {
|
||||
// Core Props
|
||||
content: string; // The text to summarize/analyze
|
||||
summary: string; // The result (bindable)
|
||||
|
||||
// Configuration (Bindable for global settings persistence)
|
||||
model?: string;
|
||||
baseUrl?: string;
|
||||
token?: string;
|
||||
systemPrompt?: string;
|
||||
maxTokens?: number;
|
||||
temperature?: number;
|
||||
|
||||
// Callbacks
|
||||
onSave?: (newSummary: string) => void;
|
||||
onSyncConfig?: () => void; // Optional: callback to sync from global site config
|
||||
|
||||
// UI Customization
|
||||
buttonClass?: string;
|
||||
log_lvl?: number;
|
||||
}
|
||||
|
||||
let {
|
||||
content,
|
||||
summary = $bindable(),
|
||||
model = $bindable(),
|
||||
baseUrl = $bindable(),
|
||||
token = $bindable(),
|
||||
systemPrompt = $bindable(),
|
||||
maxTokens = $bindable(),
|
||||
temperature = $bindable(),
|
||||
onSave,
|
||||
onSyncConfig,
|
||||
buttonClass = 'btn btn-sm preset-tonal-primary shadow-lg hover:scale-105 transition-all',
|
||||
log_lvl = 0
|
||||
}: Props = $props();
|
||||
|
||||
// Apply defaults if undefined (Safe for Svelte 5 Runes)
|
||||
if (model === undefined) model = 'dgrzone-deepseek-8b-quick';
|
||||
if (baseUrl === undefined) baseUrl = 'https://ai.dgrzone.com/api';
|
||||
if (token === undefined) token = '';
|
||||
if (systemPrompt === undefined) systemPrompt = 'You are a helpful assistant.';
|
||||
if (maxTokens === undefined) maxTokens = 512;
|
||||
if (temperature === undefined) temperature = 0.7;
|
||||
|
||||
// Internal State
|
||||
let ae_promises: any = $state(null);
|
||||
let show_modal = $state(false);
|
||||
let active_tab: 'result' | 'settings' = $state('result');
|
||||
let tmp_summary = $state('');
|
||||
|
||||
async function generate_ai_result() {
|
||||
if (!content) {
|
||||
alert('No content available to analyze.');
|
||||
return;
|
||||
}
|
||||
|
||||
let {
|
||||
content,
|
||||
summary = $bindable(),
|
||||
model = $bindable(),
|
||||
baseUrl = $bindable(),
|
||||
token = $bindable(),
|
||||
systemPrompt = $bindable(),
|
||||
maxTokens = $bindable(),
|
||||
temperature = $bindable(),
|
||||
onSave,
|
||||
onSyncConfig,
|
||||
buttonClass = "btn btn-sm preset-tonal-primary shadow-lg hover:scale-105 transition-all",
|
||||
log_lvl = 0
|
||||
}: Props = $props();
|
||||
active_tab = 'result';
|
||||
|
||||
// Apply defaults if undefined (Safe for Svelte 5 Runes)
|
||||
if (model === undefined) model = 'dgrzone-deepseek-8b-quick';
|
||||
if (baseUrl === undefined) baseUrl = 'https://ai.dgrzone.com/api';
|
||||
if (token === undefined) token = '';
|
||||
if (systemPrompt === undefined) systemPrompt = 'You are a helpful assistant.';
|
||||
if (maxTokens === undefined) maxTokens = 512;
|
||||
if (temperature === undefined) temperature = 0.7;
|
||||
|
||||
// Internal State
|
||||
let ae_promises: any = $state(null);
|
||||
let show_modal = $state(false);
|
||||
let active_tab: 'result' | 'settings' = $state('result');
|
||||
let tmp_summary = $state('');
|
||||
|
||||
async function generate_ai_result() {
|
||||
if (!content) {
|
||||
alert('No content available to analyze.');
|
||||
return;
|
||||
}
|
||||
|
||||
active_tab = 'result';
|
||||
|
||||
// If no token is provided, trigger a "Demo Mode" placeholder after a fake delay
|
||||
if (!token || token === '') {
|
||||
console.log('AE_AITools: No token provided. Entering Demo Mode.');
|
||||
ae_promises = new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
tmp_summary = `### AI Summary (DEMO MODE)\n\nThis is a placeholder summary because no API token was provided in the settings. \n\n**Original Content Length:** ${content.length} characters.\n\n**System Prompt:** ${systemPrompt}\n\n**Model:** ${model}`;
|
||||
show_modal = true;
|
||||
resolve(true);
|
||||
}, 1500);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const ai_client = new OpenAI({
|
||||
apiKey: token,
|
||||
baseURL: baseUrl,
|
||||
dangerouslyAllowBrowser: true
|
||||
// If no token is provided, trigger a "Demo Mode" placeholder after a fake delay
|
||||
if (!token || token === '') {
|
||||
console.log('AE_AITools: No token provided. Entering Demo Mode.');
|
||||
ae_promises = new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
tmp_summary = `### AI Summary (DEMO MODE)\n\nThis is a placeholder summary because no API token was provided in the settings. \n\n**Original Content Length:** ${content.length} characters.\n\n**System Prompt:** ${systemPrompt}\n\n**Model:** ${model}`;
|
||||
show_modal = true;
|
||||
resolve(true);
|
||||
}, 1500);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
ae_promises = ai_client.chat.completions.create({
|
||||
const ai_client = new OpenAI({
|
||||
apiKey: token,
|
||||
baseURL: baseUrl,
|
||||
dangerouslyAllowBrowser: true
|
||||
});
|
||||
|
||||
try {
|
||||
ae_promises = ai_client.chat.completions
|
||||
.create({
|
||||
model: model || 'dgrzone-deepseek-8b-quick',
|
||||
max_tokens: maxTokens,
|
||||
temperature: temperature,
|
||||
messages: [
|
||||
{ role: 'system', content: systemPrompt || 'You are a helpful assistant.' },
|
||||
{
|
||||
role: 'system',
|
||||
content: systemPrompt || 'You are a helpful assistant.'
|
||||
},
|
||||
{ role: 'user', content: content }
|
||||
]
|
||||
}).then((resp) => {
|
||||
const result = resp?.choices?.[0]?.message?.content || 'No result generated.';
|
||||
})
|
||||
.then((resp) => {
|
||||
const result =
|
||||
resp?.choices?.[0]?.message?.content ||
|
||||
'No result generated.';
|
||||
tmp_summary = result;
|
||||
show_modal = true;
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.error('AE_AITools: AI Error:', err);
|
||||
// Even on error, show the modal with the error message so the UI can be inspected
|
||||
tmp_summary = `### AI Error\n\nFailed to connect to the AI service.\n\n**Error:** ${err.message}\n\nCheck your Settings tab for Base URL and Token configuration.`;
|
||||
show_modal = true;
|
||||
ae_promises = Promise.resolve();
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('AE_AITools: AI Error:', err);
|
||||
// Even on error, show the modal with the error message so the UI can be inspected
|
||||
tmp_summary = `### AI Error\n\nFailed to connect to the AI service.\n\n**Error:** ${err.message}\n\nCheck your Settings tab for Base URL and Token configuration.`;
|
||||
show_modal = true;
|
||||
ae_promises = Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
function handle_save() {
|
||||
summary = tmp_summary;
|
||||
if (onSave) onSave(tmp_summary);
|
||||
show_modal = false;
|
||||
}
|
||||
function handle_save() {
|
||||
summary = tmp_summary;
|
||||
if (onSave) onSave(tmp_summary);
|
||||
show_modal = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="ae-ai-tools-wrapper inline-flex items-center gap-1">
|
||||
@@ -128,13 +143,12 @@
|
||||
type="button"
|
||||
onclick={generate_ai_result}
|
||||
class={buttonClass}
|
||||
title="Generate AI summary/analysis"
|
||||
>
|
||||
title="Generate AI summary/analysis">
|
||||
{#await ae_promises}
|
||||
<Loader class="inline-block mr-1 animate-spin" size="1.2em" />
|
||||
<Loader class="mr-1 inline-block animate-spin" size="1.2em" />
|
||||
<span class="text-sm">Processing...</span>
|
||||
{:then}
|
||||
<BotMessageSquare class="inline-block mr-1" size="1.2em" />
|
||||
<BotMessageSquare class="mr-1 inline-block" size="1.2em" />
|
||||
<span class="text-sm">Summarize</span>
|
||||
{:catch}
|
||||
<span class="text-sm text-red-500">Error</span>
|
||||
@@ -149,8 +163,7 @@
|
||||
show_modal = true;
|
||||
}}
|
||||
class="btn btn-sm variant-soft-surface shadow-md"
|
||||
title="AI Settings"
|
||||
>
|
||||
title="AI Settings">
|
||||
<Settings size="1.2em" />
|
||||
</button>
|
||||
|
||||
@@ -160,32 +173,37 @@
|
||||
title="Aether AI Assistant"
|
||||
bind:open={show_modal}
|
||||
size="lg"
|
||||
class="bg-white dark:bg-gray-800"
|
||||
>
|
||||
class="bg-white dark:bg-gray-800">
|
||||
<div class="space-y-4 p-2">
|
||||
<!-- Tab Navigation -->
|
||||
<div class="flex gap-1 border-b border-surface-500/20 pb-2">
|
||||
<button
|
||||
class="btn btn-sm {active_tab === 'result' ? 'variant-filled-primary' : 'variant-soft-surface'}"
|
||||
onclick={() => active_tab = 'result'}
|
||||
>
|
||||
<div class="border-surface-500/20 flex gap-1 border-b pb-2">
|
||||
<button
|
||||
class="btn btn-sm {active_tab === 'result'
|
||||
? 'variant-filled-primary'
|
||||
: 'variant-soft-surface'}"
|
||||
onclick={() => (active_tab = 'result')}>
|
||||
<Bot size="1.1em" class="mr-1" /> Result
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm {active_tab === 'settings' ? 'variant-filled-secondary' : 'variant-soft-surface'}"
|
||||
onclick={() => active_tab = 'settings'}
|
||||
>
|
||||
<button
|
||||
class="btn btn-sm {active_tab === 'settings'
|
||||
? 'variant-filled-secondary'
|
||||
: 'variant-soft-surface'}"
|
||||
onclick={() => (active_tab = 'settings')}>
|
||||
<Settings size="1.1em" class="mr-1" /> Settings
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if active_tab === 'result'}
|
||||
<div class="space-y-4 animate-in fade-in duration-200">
|
||||
<div class="flex gap-2 justify-start">
|
||||
<button class="btn btn-sm variant-filled-success" onclick={handle_save}>
|
||||
<div class="animate-in fade-in space-y-4 duration-200">
|
||||
<div class="flex justify-start gap-2">
|
||||
<button
|
||||
class="btn btn-sm variant-filled-success"
|
||||
onclick={handle_save}>
|
||||
<Save size="1.1em" class="mr-1" /> Save Result
|
||||
</button>
|
||||
<button class="btn btn-sm variant-ghost-primary" onclick={generate_ai_result}>
|
||||
<button
|
||||
class="btn btn-sm variant-ghost-primary"
|
||||
onclick={generate_ai_result}>
|
||||
<RotateCcw size="1.1em" class="mr-1" /> Re-run
|
||||
</button>
|
||||
</div>
|
||||
@@ -195,57 +213,84 @@
|
||||
bind:new_content={tmp_summary}
|
||||
theme_mode={$ae_loc.theme_mode}
|
||||
placeholder="AI Result will appear here..."
|
||||
class_li="p-2 border rounded-lg h-96 shadow-inner bg-surface-500/5"
|
||||
/>
|
||||
class_li="p-2 border rounded-lg h-96 shadow-inner bg-surface-500/5" />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="space-y-6 animate-in slide-in-from-left-4 duration-200">
|
||||
<div
|
||||
class="animate-in slide-in-from-left-4 space-y-6 duration-200">
|
||||
<!-- Connection Settings -->
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-sm font-bold uppercase tracking-widest text-surface-500 flex items-center gap-2">
|
||||
<h3
|
||||
class="text-surface-500 flex items-center gap-2 text-sm font-bold tracking-widest uppercase">
|
||||
<Globe size="1.1em" /> API Connection
|
||||
</h3>
|
||||
|
||||
|
||||
{#if onSyncConfig}
|
||||
<button class="btn btn-sm variant-soft-primary" onclick={onSyncConfig}>
|
||||
<Copy size="1.1em" class="mr-1" /> Sync Global Defaults
|
||||
<button
|
||||
class="btn btn-sm variant-soft-primary"
|
||||
onclick={onSyncConfig}>
|
||||
<Copy size="1.1em" class="mr-1" /> Sync Global
|
||||
Defaults
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<label class="label">
|
||||
<span>Base URL</span>
|
||||
<input type="text" bind:value={baseUrl} class="input input-sm" />
|
||||
<input
|
||||
type="text"
|
||||
bind:value={baseUrl}
|
||||
class="input input-sm" />
|
||||
</label>
|
||||
<label class="label">
|
||||
<span>Model</span>
|
||||
<input type="text" bind:value={model} class="input input-sm" />
|
||||
<input
|
||||
type="text"
|
||||
bind:value={model}
|
||||
class="input input-sm" />
|
||||
</label>
|
||||
</div>
|
||||
<label class="label">
|
||||
<span>API Token</span>
|
||||
<input type="password" bind:value={token} class="input input-sm font-mono" />
|
||||
<input
|
||||
type="password"
|
||||
bind:value={token}
|
||||
class="input input-sm font-mono" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Model Parameters -->
|
||||
<div class="space-y-4 pt-4 border-t border-surface-500/10">
|
||||
<h3 class="text-sm font-bold uppercase tracking-widest text-surface-500 flex items-center gap-2">
|
||||
<div
|
||||
class="border-surface-500/10 space-y-4 border-t pt-4">
|
||||
<h3
|
||||
class="text-surface-500 flex items-center gap-2 text-sm font-bold tracking-widest uppercase">
|
||||
<FilePenLine size="1.1em" /> Inference Parameters
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<label class="label">
|
||||
<span>Temperature ({temperature})</span>
|
||||
<input type="range" bind:value={temperature} min="0" max="1" step="0.1" class="range" />
|
||||
<input
|
||||
type="range"
|
||||
bind:value={temperature}
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.1"
|
||||
class="range" />
|
||||
</label>
|
||||
<label class="label">
|
||||
<span>Max Tokens</span>
|
||||
<input type="number" bind:value={maxTokens} class="input input-sm" />
|
||||
<input
|
||||
type="number"
|
||||
bind:value={maxTokens}
|
||||
class="input input-sm" />
|
||||
</label>
|
||||
</div>
|
||||
<label class="label">
|
||||
<span>System Prompt</span>
|
||||
<textarea bind:value={systemPrompt} class="textarea h-24 text-xs font-mono"></textarea>
|
||||
<textarea
|
||||
bind:value={systemPrompt}
|
||||
class="textarea h-24 font-mono text-xs"
|
||||
></textarea>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -253,4 +298,4 @@
|
||||
</div>
|
||||
</Modal>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,58 +1,64 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* AE_ObjectFlags.svelte
|
||||
* GENERIC Aether Object Flags & Visibility Toggles
|
||||
* Manages: alert, private, public, personal, professional, template
|
||||
*/
|
||||
import {
|
||||
Siren, MessageSquareWarning, Fingerprint,
|
||||
Globe, BookHeart, BriefcaseBusiness, NotepadTextDashed,
|
||||
Settings
|
||||
} from '@lucide/svelte';
|
||||
import { ae_loc } from '$lib/stores/ae_stores';
|
||||
/**
|
||||
* AE_ObjectFlags.svelte
|
||||
* GENERIC Aether Object Flags & Visibility Toggles
|
||||
* Manages: alert, private, public, personal, professional, template
|
||||
*/
|
||||
import {
|
||||
Siren,
|
||||
MessageSquareWarning,
|
||||
Fingerprint,
|
||||
Globe,
|
||||
BookHeart,
|
||||
BriefcaseBusiness,
|
||||
NotepadTextDashed,
|
||||
Settings
|
||||
} from '@lucide/svelte';
|
||||
import { ae_loc } from '$lib/stores/ae_stores';
|
||||
|
||||
interface Props {
|
||||
// The object containing the flags (bindable)
|
||||
obj: any;
|
||||
interface Props {
|
||||
// The object containing the flags (bindable)
|
||||
obj: any;
|
||||
|
||||
// Visibility configuration (optional overrides)
|
||||
show_labels?: boolean;
|
||||
hide_alert?: boolean;
|
||||
hide_private?: boolean;
|
||||
hide_public?: boolean;
|
||||
hide_personal?: boolean;
|
||||
hide_professional?: boolean;
|
||||
hide_template?: boolean;
|
||||
// Visibility configuration (optional overrides)
|
||||
show_labels?: boolean;
|
||||
hide_alert?: boolean;
|
||||
hide_private?: boolean;
|
||||
hide_public?: boolean;
|
||||
hide_personal?: boolean;
|
||||
hide_professional?: boolean;
|
||||
hide_template?: boolean;
|
||||
|
||||
// Callbacks
|
||||
on_toggle?: (prop: string, newValue: boolean) => void;
|
||||
// Callbacks
|
||||
on_toggle?: (prop: string, newValue: boolean) => void;
|
||||
|
||||
// Styling
|
||||
container_class?: string;
|
||||
}
|
||||
// Styling
|
||||
container_class?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
obj = $bindable(),
|
||||
show_labels = true,
|
||||
hide_alert: hide_alert = false,
|
||||
hide_private: hide_private = false,
|
||||
hide_public: hide_public = false,
|
||||
hide_personal: hide_personal = false,
|
||||
hide_professional: hide_professional = false,
|
||||
hide_template: hide_template = false,
|
||||
on_toggle: onToggle,
|
||||
container_class = "flex flex-row flex-wrap gap-1 items-center justify-evenly py-2 border-y border-surface-500/10"
|
||||
}: Props = $props();
|
||||
let {
|
||||
obj = $bindable(),
|
||||
show_labels = true,
|
||||
hide_alert: hide_alert = false,
|
||||
hide_private: hide_private = false,
|
||||
hide_public: hide_public = false,
|
||||
hide_personal: hide_personal = false,
|
||||
hide_professional: hide_professional = false,
|
||||
hide_template: hide_template = false,
|
||||
on_toggle: onToggle,
|
||||
container_class = 'flex flex-row flex-wrap gap-1 items-center justify-evenly py-2 border-y border-surface-500/10'
|
||||
}: Props = $props();
|
||||
|
||||
function handle_toggle(prop: string) {
|
||||
obj[prop] = !obj[prop];
|
||||
if (onToggle) onToggle(prop, obj[prop]);
|
||||
}
|
||||
function handle_toggle(prop: string) {
|
||||
obj[prop] = !obj[prop];
|
||||
if (onToggle) onToggle(prop, obj[prop]);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class={container_class}>
|
||||
{#if show_labels}
|
||||
<span class="text-xs text-surface-500 flex items-center gap-1 uppercase font-bold tracking-wider mr-2">
|
||||
<span
|
||||
class="text-surface-500 mr-2 flex items-center gap-1 text-xs font-bold tracking-wider uppercase">
|
||||
<Settings size="1.1em" /> Flags:
|
||||
</span>
|
||||
{/if}
|
||||
@@ -63,9 +69,10 @@
|
||||
type="button"
|
||||
onclick={() => handle_toggle('alert')}
|
||||
class="btn-icon btn-icon-sm preset-tonal-secondary hover:preset-filled-secondary-500 transition"
|
||||
title="Toggle Alert Status"
|
||||
>
|
||||
<Siren size="1.2em" class={obj?.alert ? 'text-error-500' : 'opacity-40'} />
|
||||
title="Toggle Alert Status">
|
||||
<Siren
|
||||
size="1.2em"
|
||||
class={obj?.alert ? 'text-error-500' : 'opacity-40'} />
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
@@ -75,9 +82,10 @@
|
||||
type="button"
|
||||
onclick={() => handle_toggle('private')}
|
||||
class="btn-icon btn-icon-sm preset-tonal-secondary hover:preset-filled-secondary-500 transition"
|
||||
title="Toggle Private/Encrypted"
|
||||
>
|
||||
<Fingerprint size="1.2em" class={obj?.private ? 'text-success-500' : 'opacity-40'} />
|
||||
title="Toggle Private/Encrypted">
|
||||
<Fingerprint
|
||||
size="1.2em"
|
||||
class={obj?.private ? 'text-success-500' : 'opacity-40'} />
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
@@ -87,9 +95,10 @@
|
||||
type="button"
|
||||
onclick={() => handle_toggle('public')}
|
||||
class="btn-icon btn-icon-sm preset-tonal-secondary hover:preset-filled-secondary-500 transition"
|
||||
title="Toggle Public Visibility"
|
||||
>
|
||||
<Globe size="1.2em" class={obj?.public ? 'text-success-500' : 'opacity-40'} />
|
||||
title="Toggle Public Visibility">
|
||||
<Globe
|
||||
size="1.2em"
|
||||
class={obj?.public ? 'text-success-500' : 'opacity-40'} />
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
@@ -99,9 +108,10 @@
|
||||
type="button"
|
||||
onclick={() => handle_toggle('personal')}
|
||||
class="btn-icon btn-icon-sm preset-tonal-secondary hover:preset-filled-secondary-500 transition"
|
||||
title="Toggle Personal Scope"
|
||||
>
|
||||
<BookHeart size="1.2em" class={obj?.personal ? 'text-success-500' : 'opacity-40'} />
|
||||
title="Toggle Personal Scope">
|
||||
<BookHeart
|
||||
size="1.2em"
|
||||
class={obj?.personal ? 'text-success-500' : 'opacity-40'} />
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
@@ -111,9 +121,10 @@
|
||||
type="button"
|
||||
onclick={() => handle_toggle('professional')}
|
||||
class="btn-icon btn-icon-sm preset-tonal-secondary hover:preset-filled-secondary-500 transition"
|
||||
title="Toggle Professional Scope"
|
||||
>
|
||||
<BriefcaseBusiness size="1.2em" class={obj?.professional ? 'text-success-500' : 'opacity-40'} />
|
||||
title="Toggle Professional Scope">
|
||||
<BriefcaseBusiness
|
||||
size="1.2em"
|
||||
class={obj?.professional ? 'text-success-500' : 'opacity-40'} />
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
@@ -123,9 +134,10 @@
|
||||
type="button"
|
||||
onclick={() => handle_toggle('template')}
|
||||
class="btn-icon btn-icon-sm preset-tonal-secondary hover:preset-filled-secondary-500 transition"
|
||||
title="Toggle Template Mode"
|
||||
>
|
||||
<NotepadTextDashed size="1.2em" class={obj?.template ? 'text-success-500' : 'opacity-40'} />
|
||||
title="Toggle Template Mode">
|
||||
<NotepadTextDashed
|
||||
size="1.2em"
|
||||
class={obj?.template ? 'text-success-500' : 'opacity-40'} />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,95 +1,96 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* AE_Record_Controls.svelte
|
||||
* GENERIC Aether Record Management Controls
|
||||
* Manages: priority, hide, enable, alert, delete/disable
|
||||
*
|
||||
* Emits events — NO API calls. Parent is responsible for:
|
||||
* 1. Calling the API (update_ae_obj, delete_ae_obj_id__*)
|
||||
* 2. Refreshing the object from cache/API
|
||||
* 3. Navigating away on delete
|
||||
*
|
||||
* Usage:
|
||||
* <AE_Record_Controls
|
||||
* obj={$lq__event_session_obj}
|
||||
* obj_label="session"
|
||||
* allow_delete={$ae_loc.manager_access}
|
||||
* allow_disable={$ae_loc.administrator_access}
|
||||
* on_toggle={(field, val) => { ... }}
|
||||
* on_delete={(method) => { ... }}
|
||||
* />
|
||||
*/
|
||||
import {
|
||||
Star,
|
||||
Eye,
|
||||
EyeOff,
|
||||
ToggleLeft,
|
||||
ToggleRight,
|
||||
Bell,
|
||||
BellOff,
|
||||
Trash2,
|
||||
CircleMinus,
|
||||
Settings
|
||||
} from '@lucide/svelte';
|
||||
/**
|
||||
* AE_Record_Controls.svelte
|
||||
* GENERIC Aether Record Management Controls
|
||||
* Manages: priority, hide, enable, alert, delete/disable
|
||||
*
|
||||
* Emits events — NO API calls. Parent is responsible for:
|
||||
* 1. Calling the API (update_ae_obj, delete_ae_obj_id__*)
|
||||
* 2. Refreshing the object from cache/API
|
||||
* 3. Navigating away on delete
|
||||
*
|
||||
* Usage:
|
||||
* <AE_Record_Controls
|
||||
* obj={$lq__event_session_obj}
|
||||
* obj_label="session"
|
||||
* allow_delete={$ae_loc.manager_access}
|
||||
* allow_disable={$ae_loc.administrator_access}
|
||||
* on_toggle={(field, val) => { ... }}
|
||||
* on_delete={(method) => { ... }}
|
||||
* />
|
||||
*/
|
||||
import {
|
||||
Star,
|
||||
Eye,
|
||||
EyeOff,
|
||||
ToggleLeft,
|
||||
ToggleRight,
|
||||
Bell,
|
||||
BellOff,
|
||||
Trash2,
|
||||
CircleMinus,
|
||||
Settings
|
||||
} from '@lucide/svelte';
|
||||
|
||||
interface Props {
|
||||
// The object whose flags are being displayed (read-only — parent owns state)
|
||||
obj: any;
|
||||
interface Props {
|
||||
// The object whose flags are being displayed (read-only — parent owns state)
|
||||
obj: any;
|
||||
|
||||
// Human-readable label for confirm dialogs ("session", "presenter", "location", etc.)
|
||||
obj_label?: string;
|
||||
// Human-readable label for confirm dialogs ("session", "presenter", "location", etc.)
|
||||
obj_label?: string;
|
||||
|
||||
// Visibility — hide any control that doesn't apply for this object type
|
||||
show_alert?: boolean;
|
||||
show_priority?: boolean;
|
||||
show_enable?: boolean;
|
||||
show_hide?: boolean;
|
||||
show_labels?: boolean;
|
||||
// Visibility — hide any control that doesn't apply for this object type
|
||||
show_alert?: boolean;
|
||||
show_priority?: boolean;
|
||||
show_enable?: boolean;
|
||||
show_hide?: boolean;
|
||||
show_labels?: boolean;
|
||||
|
||||
// Permission gates — parent passes booleans derived from $ae_loc
|
||||
allow_delete?: boolean; // Hard permanent delete (manager+)
|
||||
allow_disable?: boolean; // Soft disable/remove (administrator+)
|
||||
// Permission gates — parent passes booleans derived from $ae_loc
|
||||
allow_delete?: boolean; // Hard permanent delete (manager+)
|
||||
allow_disable?: boolean; // Soft disable/remove (administrator+)
|
||||
|
||||
// Callbacks — parent handles API + refresh + navigation
|
||||
on_toggle?: (field: string, new_val: boolean) => void;
|
||||
on_delete?: (method: 'delete' | 'disable') => void;
|
||||
// Callbacks — parent handles API + refresh + navigation
|
||||
on_toggle?: (field: string, new_val: boolean) => void;
|
||||
on_delete?: (method: 'delete' | 'disable') => void;
|
||||
|
||||
// Styling
|
||||
container_class?: string;
|
||||
}
|
||||
// Styling
|
||||
container_class?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
obj,
|
||||
obj_label = 'record',
|
||||
show_alert = true,
|
||||
show_priority = true,
|
||||
show_enable = true,
|
||||
show_hide = true,
|
||||
show_labels = true,
|
||||
allow_delete = false,
|
||||
allow_disable = false,
|
||||
on_toggle,
|
||||
on_delete,
|
||||
container_class = 'flex flex-row flex-wrap gap-1 items-center justify-evenly py-2 border-y border-surface-500/10'
|
||||
}: Props = $props();
|
||||
let {
|
||||
obj,
|
||||
obj_label = 'record',
|
||||
show_alert = true,
|
||||
show_priority = true,
|
||||
show_enable = true,
|
||||
show_hide = true,
|
||||
show_labels = true,
|
||||
allow_delete = false,
|
||||
allow_disable = false,
|
||||
on_toggle,
|
||||
on_delete,
|
||||
container_class = 'flex flex-row flex-wrap gap-1 items-center justify-evenly py-2 border-y border-surface-500/10'
|
||||
}: Props = $props();
|
||||
|
||||
function toggle(field: string) {
|
||||
if (on_toggle) on_toggle(field, !obj?.[field]);
|
||||
}
|
||||
function toggle(field: string) {
|
||||
if (on_toggle) on_toggle(field, !obj?.[field]);
|
||||
}
|
||||
|
||||
function handle_delete(method: 'delete' | 'disable') {
|
||||
const msg =
|
||||
method === 'delete'
|
||||
? `Permanently delete this ${obj_label}? This cannot be undone.`
|
||||
: `Remove (disable) this ${obj_label}?`;
|
||||
if (!confirm(msg)) return;
|
||||
if (on_delete) on_delete(method);
|
||||
}
|
||||
function handle_delete(method: 'delete' | 'disable') {
|
||||
const msg =
|
||||
method === 'delete'
|
||||
? `Permanently delete this ${obj_label}? This cannot be undone.`
|
||||
: `Remove (disable) this ${obj_label}?`;
|
||||
if (!confirm(msg)) return;
|
||||
if (on_delete) on_delete(method);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class={container_class}>
|
||||
{#if show_labels}
|
||||
<span class="text-xs text-surface-500 flex items-center gap-1 uppercase font-bold tracking-wider mr-2">
|
||||
<span
|
||||
class="text-surface-500 mr-2 flex items-center gap-1 text-xs font-bold tracking-wider uppercase">
|
||||
<Settings size="1.1em" /> Controls:
|
||||
</span>
|
||||
{/if}
|
||||
@@ -103,12 +104,10 @@
|
||||
class:preset-filled-warning-500={obj?.priority}
|
||||
class:preset-tonal-secondary={!obj?.priority}
|
||||
class:hover:preset-filled-warning-500={!obj?.priority}
|
||||
title={obj?.priority ? 'Remove priority flag' : 'Mark as priority'}
|
||||
>
|
||||
title={obj?.priority ? 'Remove priority flag' : 'Mark as priority'}>
|
||||
<Star
|
||||
size="1.2em"
|
||||
class={obj?.priority ? 'fill-current' : 'opacity-50'}
|
||||
/>
|
||||
class={obj?.priority ? 'fill-current' : 'opacity-50'} />
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
@@ -121,8 +120,7 @@
|
||||
class:preset-filled-warning-500={obj?.hide}
|
||||
class:preset-tonal-secondary={!obj?.hide}
|
||||
class:hover:preset-filled-warning-500={!obj?.hide}
|
||||
title={obj?.hide ? 'Unhide this record' : 'Hide this record'}
|
||||
>
|
||||
title={obj?.hide ? 'Unhide this record' : 'Hide this record'}>
|
||||
{#if obj?.hide}
|
||||
<EyeOff size="1.2em" class="text-warning-500" />
|
||||
{:else}
|
||||
@@ -140,8 +138,7 @@
|
||||
class:preset-filled-success-500={obj?.enable}
|
||||
class:preset-filled-error-500={!obj?.enable}
|
||||
class:hover:preset-filled-success-500={!obj?.enable}
|
||||
title={obj?.enable ? 'Disable this record' : 'Enable this record'}
|
||||
>
|
||||
title={obj?.enable ? 'Disable this record' : 'Enable this record'}>
|
||||
{#if obj?.enable}
|
||||
<ToggleRight size="1.2em" class="text-success-300" />
|
||||
{:else}
|
||||
@@ -159,8 +156,7 @@
|
||||
class:preset-filled-error-500={obj?.alert}
|
||||
class:preset-tonal-secondary={!obj?.alert}
|
||||
class:hover:preset-filled-error-500={!obj?.alert}
|
||||
title={obj?.alert ? 'Remove alert status' : 'Mark as alert'}
|
||||
>
|
||||
title={obj?.alert ? 'Remove alert status' : 'Mark as alert'}>
|
||||
{#if obj?.alert}
|
||||
<Bell size="1.2em" class="text-error-300" />
|
||||
{:else}
|
||||
@@ -175,8 +171,7 @@
|
||||
type="button"
|
||||
onclick={() => handle_delete('delete')}
|
||||
class="btn-icon btn-icon-sm preset-filled-error-500 hover:preset-filled-error-600 transition"
|
||||
title="Permanently delete this {obj_label}"
|
||||
>
|
||||
title="Permanently delete this {obj_label}">
|
||||
<Trash2 size="1.2em" />
|
||||
</button>
|
||||
{:else if allow_disable}
|
||||
@@ -184,8 +179,7 @@
|
||||
type="button"
|
||||
onclick={() => handle_delete('disable')}
|
||||
class="btn-icon btn-icon-sm preset-filled-warning-500 hover:preset-filled-warning-600 transition"
|
||||
title="Disable / soft-remove this {obj_label}"
|
||||
>
|
||||
title="Disable / soft-remove this {obj_label}">
|
||||
<CircleMinus size="1.2em" />
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user