256 lines
11 KiB
Svelte
256 lines
11 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, 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/AE_Comp_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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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
|
|
});
|
|
|
|
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: 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;
|
|
}
|
|
</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="inline-block mr-1 animate-spin" size="1.2em" />
|
|
<span class="text-sm">Processing...</span>
|
|
{:then}
|
|
<BotMessageSquare class="inline-block mr-1" 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 variant-soft-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="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'}
|
|
>
|
|
<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'}
|
|
>
|
|
<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}>
|
|
<Save size="1.1em" class="mr-1" /> Save Result
|
|
</button>
|
|
<button class="btn btn-sm variant-ghost-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="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>
|
|
</Modal>
|
|
{/if}
|
|
</div> |