Centralize AI configuration into generic AE_AITools component

- Enhanced AE_AITools with a Settings tab for model, prompt, and parameter configuration.
- Connected AE_AITools to Journals state via two-way bindings for persistent configuration.
- Removed redundant AI settings from Journals config modal.
- Standardized Svelte 5 patterns for cross-module component configuration.
This commit is contained in:
Scott Idem
2026-01-08 18:12:15 -05:00
parent c884eed52c
commit 51bdad9485
3 changed files with 123 additions and 173 deletions

View File

@@ -2,13 +2,14 @@
/** /**
* AE_AITools.svelte * AE_AITools.svelte
* GENERIC Aether AI Toolset (Runes/Svelte 5) * GENERIC Aether AI Toolset (Runes/Svelte 5)
* Reusable across all modules (Journals, Events, People, etc.) * Extracted logic from Journals module to be system-wide.
*/ */
import OpenAI from 'openai'; import OpenAI from 'openai';
import { Modal } from 'flowbite-svelte'; import { Modal } from 'flowbite-svelte';
import { import {
Bot, BotMessageSquare, Loader, FileText, Bot, BotMessageSquare, Loader, FileText,
Save, FilePenLine, RotateCcw Save, FilePenLine, RotateCcw, Settings,
RefreshCcw, Globe, Copy
} from '@lucide/svelte'; } from '@lucide/svelte';
import { ae_loc, ae_api } from '$lib/stores/ae_stores'; import { ae_loc, ae_api } from '$lib/stores/ae_stores';
import E_app_codemirror_v5 from '$lib/app_components/e_app_codemirror_v5.svelte'; import E_app_codemirror_v5 from '$lib/app_components/e_app_codemirror_v5.svelte';
@@ -18,61 +19,68 @@
content: string; // The text to summarize/analyze content: string; // The text to summarize/analyze
summary: string; // The result (bindable) summary: string; // The result (bindable)
// Configuration // Configuration (Bindable for global settings persistence)
model?: string; model?: string;
baseUrl?: string; baseUrl?: string;
token?: string; token?: string;
systemPrompt?: string; systemPrompt?: string;
maxTokens?: number;
temperature?: number;
// Callbacks // Callbacks
onSave?: (newSummary: string) => void; onSave?: (newSummary: string) => void;
onSyncConfig?: () => void; // Optional: callback to sync from global site config
// UI Customization // UI Customization
buttonClass?: string; buttonClass?: string;
showSavedButton?: boolean;
log_lvl?: number; log_lvl?: number;
} }
let { let {
content, content,
summary = $bindable(), summary = $bindable(),
model = '', model = $bindable('dgrzone-deepseek-8b-quick'),
baseUrl = '', baseUrl = $bindable('https://ai.dgrzone.com/api'),
token = '', token = $bindable(''),
systemPrompt = 'You are a helpful assistant that summarizes technical content.', systemPrompt = $bindable('You are a helpful assistant.'),
maxTokens = $bindable(512),
temperature = $bindable(0.7),
onSave, onSave,
onSyncConfig,
buttonClass = "btn btn-sm preset-tonal-primary shadow-lg hover:scale-105 transition-all", buttonClass = "btn btn-sm preset-tonal-primary shadow-lg hover:scale-105 transition-all",
showSavedButton = true,
log_lvl = 0 log_lvl = 0
}: Props = $props(); }: Props = $props();
// Internal State // Internal State
let ae_promises: any = $state(null); let ae_promises: any = $state(null);
let show_modal = $state(false); let show_modal = $state(false);
let active_tab: 'result' | 'settings' = $state('result');
let tmp_summary = $state(''); let tmp_summary = $state('');
async function generate_ai_result() { async function generate_ai_result() {
if (!content) { if (!content) {
alert('No content available to summarize.'); alert('No content available to analyze.');
return; return;
} }
const ai_client = new OpenAI({ const ai_client = new OpenAI({
apiKey: token || 'no-token-provided', apiKey: token || 'no-token-provided',
baseURL: baseUrl || 'https://ai.dgrzone.com/api', baseURL: baseUrl,
dangerouslyAllowBrowser: true dangerouslyAllowBrowser: true
}); });
try { try {
active_tab = 'result';
ae_promises = ai_client.chat.completions.create({ ae_promises = ai_client.chat.completions.create({
model: model || 'dgrzone-deepseek-8b-quick', model: model,
max_tokens: maxTokens,
temperature: temperature,
messages: [ messages: [
{ role: 'system', content: systemPrompt }, { role: 'system', content: systemPrompt },
{ role: 'user', content: content } { role: 'user', content: content }
] ]
}).then((resp) => { }).then((resp) => {
const result = resp?.choices?.[0]?.message?.content || 'No result generated.'; const result = resp?.choices?.[0]?.message?.content || 'No result generated.';
if (log_lvl) console.log('AE_AITools: Result generated', result);
tmp_summary = result; tmp_summary = result;
show_modal = true; show_modal = true;
}); });
@@ -89,7 +97,7 @@
} }
</script> </script>
<div class="ae-ai-tools-wrapper"> <div class="ae-ai-tools-wrapper inline-block">
<!-- Trigger Button --> <!-- Trigger Button -->
<button <button
type="button" type="button"
@@ -108,45 +116,104 @@
{/await} {/await}
</button> </button>
<!-- Modal for Result Review --> <!-- Unified AI Modal -->
{#if show_modal} {#if show_modal}
<Modal <Modal
title="AI Generated Summary" title="Aether AI Assistant"
bind:open={show_modal} bind:open={show_modal}
size="lg" size="lg"
class="bg-white dark:bg-gray-800" class="bg-white dark:bg-gray-800"
> >
<div class="space-y-4 p-2"> <div class="space-y-4 p-2">
<div class="flex gap-2 justify-between items-center border-b border-surface-500/20 pb-2"> <!-- Tab Navigation -->
<div class="flex gap-2"> <div class="flex gap-1 border-b border-surface-500/20 pb-2">
<button class="btn btn-sm variant-filled-success" onclick={handle_save}> <button
<Save size="1.1em" class="mr-1" /> Save Result class="btn btn-sm {active_tab === 'result' ? 'variant-filled-primary' : 'variant-soft-surface'}"
</button> onclick={() => active_tab = 'result'}
<button class="btn btn-sm variant-ghost-primary" onclick={generate_ai_result}> >
<RotateCcw size="1.1em" class="mr-1" /> Re-run <Bot size="1.1em" class="mr-1" /> Result
</button> </button>
</div> <button
<span class="text-xs opacity-50 uppercase font-bold tracking-tighter"> class="btn btn-sm {active_tab === 'settings' ? 'variant-filled-secondary' : 'variant-soft-surface'}"
{model || 'Default Model'} onclick={() => active_tab = 'settings'}
</span> >
<Settings size="1.1em" class="mr-1" /> Settings
</button>
</div> </div>
<E_app_codemirror_v5 {#if active_tab === 'result'}
editable={true} <div class="space-y-4 animate-in fade-in duration-200">
content={tmp_summary} <div class="flex gap-2 justify-start">
bind:new_content={tmp_summary} <button class="btn btn-sm variant-filled-success" onclick={handle_save}>
bind:theme_mode={$ae_loc.theme_mode} <Save size="1.1em" class="mr-1" /> Save Result
placeholder="AI Result will appear here..." </button>
class="p-2 border rounded-lg h-96 shadow-inner bg-surface-500/5" <button class="btn btn-sm variant-ghost-primary" onclick={generate_ai_result}>
/> <RotateCcw size="1.1em" class="mr-1" /> Re-run
</button>
</div>
<E_app_codemirror_v5
editable={true}
content={tmp_summary}
bind:new_content={tmp_summary}
bind:theme_mode={$ae_loc.theme_mode}
placeholder="AI Result will appear here..."
class="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">
<!-- Connection Settings -->
<div class="space-y-4">
<h3 class="text-sm font-bold uppercase tracking-widest text-surface-500 flex items-center gap-2">
<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>
{/if}
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<label class="label">
<span>Base URL</span>
<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" />
</label>
</div>
<label class="label">
<span>API Token</span>
<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">
<FilePenLine size="1.1em" /> Inference Parameters
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<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 text-xs font-mono"></textarea>
</label>
</div>
</div>
{/if}
</div> </div>
</Modal> </Modal>
{/if} {/if}
</div> </div>
<style>
/* Add any generic wrapper styles here if needed */
.ae-ai-tools-wrapper {
display: inline-block;
}
</style>

View File

@@ -1718,10 +1718,18 @@
content={tmp_entry_obj.content} content={tmp_entry_obj.content}
bind:summary={tmp_entry_obj.summary} bind:summary={tmp_entry_obj.summary}
onSave={update_journal_entry} onSave={update_journal_entry}
token={$journals_loc?.llm__api_token} bind:token={$journals_loc.llm__api_token}
baseUrl={$journals_loc?.llm__api_base_url} bind:baseUrl={$journals_loc.llm__api_base_url}
model={$journals_loc?.llm__api_model} bind:model={$journals_loc.llm__api_model}
systemPrompt={$journals_loc?.entry?.llm__system_prompt} bind:systemPrompt={$journals_loc.entry.llm__system_prompt}
bind:maxTokens={$journals_loc.entry.llm__max_tokens}
bind:temperature={$journals_loc.entry.llm__temperature}
onSyncConfig={() => {
$journals_loc.llm__api_base_url = $ae_loc.site_cfg_json?.llm__api_base_url ?? 'https://ai.dgrzone.com/api';
$journals_loc.llm__api_model = $ae_loc.site_cfg_json?.llm__api_model ?? 'dgrzone-deepseek-8b-quick';
$journals_loc.llm__api_token = $ae_loc.site_cfg_json?.llm__api_token ?? '';
$journals_loc.entry.llm__system_prompt = $ae_loc.site_cfg_json?.llm__system_prompt ?? '';
}}
{log_lvl} {log_lvl}
/> />
</div> </div>

View File

@@ -202,131 +202,6 @@
</label> </label>
</div> </div>
</div> </div>
<div class="w-full space-y-4">
<h2 class="h2 text-xl font-bold flex items-center gap-2 border-b border-surface-500/30 pb-2">
<Bot class="text-primary-500" />
AI Summary Configuration
</h2>
<div class="space-y-4 bg-surface-500/5 p-4 rounded-lg">
<button
type="button"
class="btn variant-ghost-primary w-full md:w-auto"
onclick={() => {
$journals_loc.llm__api_base_url =
$ae_loc.site_cfg_json?.llm__api_base_url ??
'https://ai.dgrzone.com/api';
$journals_loc.llm__api_model =
$ae_loc.site_cfg_json?.llm__api_model ??
'dgrzone-deepseek-8b-quick';
$journals_loc.llm__api_token =
$ae_loc.site_cfg_json?.llm__api_token ?? '';
$journals_loc.llm__api_dangerous_browser =
$ae_loc.site_cfg_json?.llm__api_dangerous_browser ?? false;
$journals_loc.entry.llm__system_prompt =
$ae_loc.site_cfg_json?.llm__system_prompt ?? '';
}}
>
<Copy size="1em" class="mr-2" />
Sync from Site Configuration
</button>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<label class="label">
<span>LLM API Base URL</span>
<input
type="text"
bind:value={$journals_loc.llm__api_base_url}
placeholder="https://..."
class="input"
/>
</label>
<label class="label">
<span>LLM Model</span>
<input
type="text"
bind:value={$journals_loc.llm__api_model}
class="input"
/>
</label>
</div>
<label class="label">
<span>LLM API Token</span>
<input
type="password"
bind:value={$journals_loc.llm__api_token}
class="input font-mono"
/>
</label>
</div>
</div>
<div class="w-full space-y-2 p-2 border-t border-surface-100-900">
<h2 class="font-semibold">$journals_loc?.entry:</h2>
<!-- entry > ai__system_prompt - textarea -->
<!-- <div class="h-16"> -->
<!-- </div> -->
<!-- entry > llm__max_tokens - number -->
<label class="flex items-center justify-start gap-1">
LLM Max Tokens:
<input
type="number"
bind:value={$journals_loc.entry.llm__max_tokens}
placeholder="LLM Max Tokens"
class="input input-sm input-bordered w-full max-w-xs"
/>
</label>
<!-- entry > llm__temperature - number -->
<label class="flex items-center justify-start gap-1">
LLM Temperature:
<input
type="number"
step="0.01"
min="0"
max="1"
bind:value={$journals_loc.entry.llm__temperature}
placeholder="LLM Temperature"
class="input input-sm input-bordered w-full max-w-xs"
/>
</label>
<label class="flex flex-col items-start justify-start gap-1 h-24">
LLM System Prompt for Journal Entry summarization:
<E_app_codemirror_v5
editable={true}
readonly={false}
content={$journals_loc?.entry?.llm__system_prompt}
bind:new_content={$journals_loc.entry.llm__system_prompt}
show_line_numbers={false}
placeholder="LLM System Prompt"
class="
p-1
preset-outlined-success-400-600
shadow-lg rounded-lg
"
/>
</label>
<button
type="button"
class="btn btn-sm preset-outlined-primary-400-600"
onclick={() => {
let default_system_prompt =
'Summarize the following journal entry content in a concise manner, focusing on key points and insights.';
$journals_loc.entry.llm__system_prompt = default_system_prompt;
}}
>
<RefreshCcw class="inline-block mr-1" />
Reset to Journal Entry System Prompt
</button>
</div>
</div> </div>
<!-- Section for viewing (and direct editing???) the raw localStorage JSON configuration --> <!-- Section for viewing (and direct editing???) the raw localStorage JSON configuration -->