Polish the Journal Entry Config modal to match the desired section outline, hide alert messaging unless enabled, update the shared draft typing for entry flows, and replace deprecated privacy icons. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
347 lines
14 KiB
Svelte
347 lines
14 KiB
Svelte
<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,
|
|
Eye,
|
|
EyeOff,
|
|
Globe,
|
|
Copy,
|
|
Loader,
|
|
Save,
|
|
FilePenLine,
|
|
RotateCcw,
|
|
Settings,
|
|
} from '@lucide/svelte';
|
|
import { ae_loc } from '$lib/stores/ae_stores';
|
|
import AE_Comp_Editor_CodeMirror from '$lib/elements/element_editor_codemirror.svelte';
|
|
|
|
interface Props {
|
|
// Core Props
|
|
content: string | null | undefined; // The text to summarize/analyze
|
|
summary: string | null | undefined; // 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 = $state<Promise<unknown> | null>(null);
|
|
let show_modal = $state(false);
|
|
let active_tab: 'result' | 'settings' = $state('result');
|
|
let tmp_summary = $state('');
|
|
let show_api_token = $state(false);
|
|
|
|
const panel_class = 'space-y-4 rounded-xl border border-surface-500/20 bg-surface-500/5 p-4 shadow-sm';
|
|
const panel_title_class = 'flex items-center gap-2 border-b border-surface-500/20 pb-2 text-lg font-bold';
|
|
const tab_button_base_class = 'btn btn-sm border transition-all duration-200';
|
|
const tab_button_active_class = 'border-surface-200-800 bg-surface-200-800 text-surface-950-50 shadow-sm';
|
|
const tab_button_inactive_class = 'border-transparent bg-surface-50-900/60 text-surface-600-400 hover:border-surface-200-800 hover:bg-surface-100-900 hover:text-surface-950-50';
|
|
|
|
function tab_button_class(is_active: boolean): string {
|
|
return `${tab_button_base_class} ${is_active ? tab_button_active_class : tab_button_inactive_class}`;
|
|
}
|
|
|
|
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 === '') {
|
|
if (log_lvl > 0) {
|
|
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
|
|
});
|
|
|
|
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: 'user', content: content }
|
|
]
|
|
})
|
|
.then((resp) => {
|
|
const result =
|
|
resp?.choices?.[0]?.message?.content ||
|
|
'No result generated.';
|
|
tmp_summary = result;
|
|
show_modal = true;
|
|
});
|
|
} catch (err: unknown) {
|
|
if (log_lvl > 0) {
|
|
console.error('AE_AITools: AI Error:', err);
|
|
}
|
|
const err_msg = err instanceof Error ? err.message : String(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_msg}\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;
|
|
}
|
|
</script>
|
|
|
|
<div class="ae-ai-tools-wrapper inline-flex items-center gap-1">
|
|
<!-- Trigger Button -->
|
|
<button
|
|
type="button"
|
|
onclick={generate_ai_result}
|
|
class={buttonClass}
|
|
title="Generate AI summary/analysis">
|
|
{#await ae_promises}
|
|
<Loader class="mr-1 inline-block animate-spin" size="1.2em" />
|
|
<span class="text-sm">Processing...</span>
|
|
{:then}
|
|
<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>
|
|
{/await}
|
|
</button>
|
|
|
|
<!-- Quick Settings Button -->
|
|
<button
|
|
type="button"
|
|
onclick={() => {
|
|
active_tab = 'settings';
|
|
show_modal = true;
|
|
}}
|
|
class="btn btn-sm preset-tonal-surface shadow-md"
|
|
title="AI Settings">
|
|
<Settings size="1.2em" />
|
|
</button>
|
|
|
|
<!-- Unified AI Modal -->
|
|
{#if show_modal}
|
|
<Modal
|
|
title="Aether AI Assistant"
|
|
bind:open={show_modal}
|
|
size="lg"
|
|
class="relative mx-auto flex h-[calc(100dvh-2rem)] max-h-[calc(100dvh-2rem)] w-full flex-col rounded-xl border border-surface-200-800 bg-surface-50-900 text-surface-950-50 shadow-xl">
|
|
<div class="min-h-0 flex-1 space-y-4 overflow-y-auto px-4 py-3">
|
|
<!-- Tab Navigation -->
|
|
<div class="bg-surface-500/10 sticky top-0 z-10 mx-auto flex max-w-fit justify-center gap-1 rounded-lg p-1 backdrop-blur-sm">
|
|
<button
|
|
class={tab_button_class(active_tab === 'result')}
|
|
onclick={() => (active_tab = 'result')}>
|
|
<Bot size="1.1em" class="mr-1" /> Result
|
|
</button>
|
|
<button
|
|
class={tab_button_class(active_tab === 'settings')}
|
|
onclick={() => (active_tab = 'settings')}>
|
|
<Settings size="1.1em" class="mr-1" /> Settings
|
|
</button>
|
|
</div>
|
|
|
|
{#if active_tab === 'result'}
|
|
<div class="animate-in fade-in space-y-4 duration-200">
|
|
<div class="flex justify-start gap-2">
|
|
<button
|
|
class="btn btn-sm preset-filled-success"
|
|
onclick={handle_save}>
|
|
<Save size="1.1em" class="mr-1" /> Save Result
|
|
</button>
|
|
<button
|
|
class="btn btn-sm preset-tonal-primary"
|
|
onclick={generate_ai_result}>
|
|
<RotateCcw size="1.1em" class="mr-1" /> Re-run
|
|
</button>
|
|
</div>
|
|
|
|
<AE_Comp_Editor_CodeMirror
|
|
content={tmp_summary}
|
|
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" />
|
|
</div>
|
|
{:else}
|
|
<div
|
|
class="animate-in slide-in-from-left-4 space-y-6 duration-200">
|
|
<!-- Connection Settings -->
|
|
<section class={panel_class}>
|
|
<h3 class={panel_title_class}>
|
|
<Globe size="1.1em" /> API Connection
|
|
</h3>
|
|
|
|
{#if onSyncConfig}
|
|
<button
|
|
class="btn btn-sm preset-tonal-primary"
|
|
onclick={onSyncConfig}>
|
|
<Copy size="1.1em" class="mr-1" /> Sync Global
|
|
Defaults
|
|
</button>
|
|
{/if}
|
|
|
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
<label class="label">
|
|
<span>Base URL</span>
|
|
<input
|
|
type="text"
|
|
autocomplete="off"
|
|
autocapitalize="off"
|
|
autocorrect="off"
|
|
spellcheck="false"
|
|
bind:value={baseUrl}
|
|
class="input input-sm" />
|
|
</label>
|
|
<label class="label">
|
|
<span>Model</span>
|
|
<input
|
|
type="text"
|
|
autocomplete="off"
|
|
autocapitalize="off"
|
|
autocorrect="off"
|
|
spellcheck="false"
|
|
name="ai_model"
|
|
data-bwignore="true"
|
|
data-lpignore="true"
|
|
data-1p-ignore="true"
|
|
bind:value={model}
|
|
class="input input-sm" />
|
|
</label>
|
|
</div>
|
|
<label class="label">
|
|
<span>API Token</span>
|
|
<div class="flex gap-2">
|
|
<input
|
|
type={show_api_token ? 'text' : 'password'}
|
|
autocomplete="off"
|
|
autocapitalize="off"
|
|
autocorrect="off"
|
|
spellcheck="false"
|
|
name="ai_api_token"
|
|
data-bwignore="true"
|
|
data-lpignore="true"
|
|
data-1p-ignore="true"
|
|
bind:value={token}
|
|
class="input input-sm font-mono" />
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm preset-tonal-surface"
|
|
onclick={() =>
|
|
(show_api_token = !show_api_token)}
|
|
title={show_api_token
|
|
? 'Hide API Token'
|
|
: 'Show API Token'}>
|
|
{#if show_api_token}
|
|
<EyeOff size="1.1em" />
|
|
{:else}
|
|
<Eye size="1.1em" />
|
|
{/if}
|
|
</button>
|
|
</div>
|
|
</label>
|
|
</section>
|
|
|
|
<!-- Model Parameters -->
|
|
<section class={panel_class}>
|
|
<h3 class={panel_title_class}>
|
|
<FilePenLine size="1.1em" /> Inference Parameters
|
|
</h3>
|
|
<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" />
|
|
</label>
|
|
<label class="label">
|
|
<span>Max Tokens</span>
|
|
<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 font-mono text-xs bg-surface-50-900"
|
|
></textarea>
|
|
</label>
|
|
</section>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</Modal>
|
|
{/if}
|
|
</div>
|