mirror of
https://github.com/AventurasTeam/Aventuras.git
synced 2026-04-28 03:40:11 +00:00
rename UI to aventuras
This commit is contained in:
parent
2d82d6b0e4
commit
0b447bc87b
14 changed files with 635 additions and 380 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -1,5 +1,5 @@
|
|||
# =============================================================================
|
||||
# Aventura .gitignore
|
||||
# Aventuras .gitignore
|
||||
# =============================================================================
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
|
@ -55,7 +55,7 @@ src-tauri/Cargo.lock.bak
|
|||
*.sqlite
|
||||
*.sqlite3
|
||||
|
||||
# Aventura story files (user data, not source code)
|
||||
# Aventuras story files (user data, not source code)
|
||||
*.avt
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# AGENTS.md
|
||||
|
||||
This file contains guidelines for agentic coding assistants working on the Aventura codebase.
|
||||
This file contains guidelines for agentic coding assistants working on the Aventuras codebase.
|
||||
|
||||
## Build/Lint/Test Commands
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Aventura",
|
||||
"productName": "Aventuras",
|
||||
"version": "0.5.0",
|
||||
"identifier": "com.karelian.aventura",
|
||||
"build": {
|
||||
|
|
@ -12,7 +12,7 @@
|
|||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "Aventura",
|
||||
"title": "Aventuras",
|
||||
"width": 1280,
|
||||
"height": 800,
|
||||
"minWidth": 800,
|
||||
|
|
@ -43,9 +43,9 @@
|
|||
"plugins": {
|
||||
"updater": {
|
||||
"endpoints": [
|
||||
"https://github.com/unkarelian/Aventura/releases/latest/download/latest.json"
|
||||
"https://github.com/unkarelian/Aventuras/releases/latest/download/latest.json"
|
||||
],
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDJCNUQyM0I1NzM5MEQ2MDgKUldRSTFwQnp0U05kSzdwVUV5dEYvc3ZaaS9ZWGJDcytQaERmWDhNSGJ0OTdHVlE1UkttUW5NTy8K"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
33
src/app.html
33
src/app.html
|
|
@ -1,16 +1,21 @@
|
|||
<!doctype html>
|
||||
<html lang="en" class="dark">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Aventura</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover" class="overflow-hidden">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Aventuras</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap"
|
||||
rel="stylesheet">
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
|
||||
<body data-sveltekit-preload-data="hover" class="overflow-hidden">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -82,7 +82,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
async function exportAventura() {
|
||||
async function exportAventuras() {
|
||||
if (!story.currentStory) return;
|
||||
const [
|
||||
entries,
|
||||
|
|
@ -109,7 +109,7 @@
|
|||
]);
|
||||
await handleExport(
|
||||
() =>
|
||||
exportService.exportToAventura(
|
||||
exportService.exportToAventuras(
|
||||
story.currentStory,
|
||||
entries,
|
||||
characters,
|
||||
|
|
@ -122,7 +122,7 @@
|
|||
branches,
|
||||
chapters,
|
||||
),
|
||||
"Aventura (.avt)",
|
||||
"Aventuras (.avt)",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -157,7 +157,7 @@
|
|||
<div class="flex items-center min-w-0">
|
||||
<div class="flex items-center gap-3 px-2.5 sm:px-1">
|
||||
{#if story.currentStory}
|
||||
<img src="/logo.png" alt="Aventura" class="h-7 w-7 flex-shrink-0" />
|
||||
<img src="/logo.png" alt="Aventuras" class="h-7 w-7 flex-shrink-0" />
|
||||
<span
|
||||
class="font-semibold text-surface-100 text-sm sm:text-base truncate max-w-[160px] sm:max-w-none"
|
||||
>
|
||||
|
|
@ -170,8 +170,8 @@
|
|||
{/if}
|
||||
{:else}
|
||||
<!-- App Branding (Library Mode) -->
|
||||
<img src="/logo.png" alt="Aventura" class="h-7 w-7 flex-shrink-0" />
|
||||
<span class="font-semibold text-surface-100 text-lg">Aventura</span>
|
||||
<img src="/logo.png" alt="Aventuras" class="h-7 w-7 flex-shrink-0" />
|
||||
<span class="font-semibold text-surface-100 text-lg">Aventuras</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -245,11 +245,11 @@
|
|||
class="flex w-full items-center gap-2 px-3 py-3 sm:py-2 text-left text-sm text-surface-300 hover:bg-surface-700 min-h-[44px] pointer-events-auto cursor-pointer"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
exportAventura();
|
||||
exportAventuras();
|
||||
}}
|
||||
>
|
||||
<FileJson class="h-4 w-4 text-accent-400" />
|
||||
Aventura (.avt)
|
||||
Aventuras (.avt)
|
||||
</button>
|
||||
<button
|
||||
class="flex w-full items-center gap-2 px-3 py-3 sm:py-2 text-left text-sm text-surface-300 hover:bg-surface-700 min-h-[44px] pointer-events-auto cursor-pointer"
|
||||
|
|
|
|||
|
|
@ -1,17 +1,24 @@
|
|||
<script lang="ts">
|
||||
import { ui } from '$lib/stores/ui.svelte';
|
||||
import { story } from '$lib/stores/story.svelte';
|
||||
import { ui } from "$lib/stores/ui.svelte";
|
||||
import { story } from "$lib/stores/story.svelte";
|
||||
import {
|
||||
parseLorebook,
|
||||
classifyEntriesWithLLM,
|
||||
convertToEntries,
|
||||
type ImportedEntry,
|
||||
type LorebookImportResult
|
||||
} from '$lib/services/lorebookImporter';
|
||||
import { database } from '$lib/services/database';
|
||||
import { X, Upload, FileJson, Loader2, Check, AlertCircle } from 'lucide-svelte';
|
||||
import { swipe } from '$lib/utils/swipe';
|
||||
import type { Entry } from '$lib/types';
|
||||
type LorebookImportResult,
|
||||
} from "$lib/services/lorebookImporter";
|
||||
import { database } from "$lib/services/database";
|
||||
import {
|
||||
X,
|
||||
Upload,
|
||||
FileJson,
|
||||
Loader2,
|
||||
Check,
|
||||
AlertCircle,
|
||||
} from "lucide-svelte";
|
||||
import { swipe } from "$lib/utils/swipe";
|
||||
import type { Entry } from "$lib/types";
|
||||
|
||||
let fileInput: HTMLInputElement;
|
||||
let dragOver = $state(false);
|
||||
|
|
@ -27,10 +34,13 @@
|
|||
// Type counts for preview
|
||||
const typeCounts = $derived.by(() => {
|
||||
if (!parseResult) return {};
|
||||
return parseResult.entries.reduce((acc, e) => {
|
||||
acc[e.type] = (acc[e.type] || 0) + 1;
|
||||
return acc;
|
||||
}, {} as Record<string, number>);
|
||||
return parseResult.entries.reduce(
|
||||
(acc, e) => {
|
||||
acc[e.type] = (acc[e.type] || 0) + 1;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, number>,
|
||||
);
|
||||
});
|
||||
|
||||
function handleFileSelect(e: Event) {
|
||||
|
|
@ -65,8 +75,8 @@
|
|||
|
||||
// Check file extension (case-insensitive)
|
||||
const fileName = file.name.toLowerCase();
|
||||
if (!fileName.endsWith('.json') && !fileName.endsWith('.avt')) {
|
||||
error = 'Please select a JSON or Aventura file (.json or .avt)';
|
||||
if (!fileName.endsWith(".json") && !fileName.endsWith(".avt")) {
|
||||
error = "Please select a JSON or Aventuras file (.json or .avt)";
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -75,20 +85,21 @@
|
|||
const result = parseLorebook(text);
|
||||
|
||||
if (!result.success) {
|
||||
error = result.errors.length > 0
|
||||
? result.errors.join(', ')
|
||||
: 'Invalid lorebook file format';
|
||||
error =
|
||||
result.errors.length > 0
|
||||
? result.errors.join(", ")
|
||||
: "Invalid lorebook file format";
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.entries.length === 0) {
|
||||
error = 'No valid entries found in this lorebook file';
|
||||
error = "No valid entries found in this lorebook file";
|
||||
return;
|
||||
}
|
||||
|
||||
parseResult = result;
|
||||
} catch (err) {
|
||||
error = err instanceof Error ? err.message : 'Failed to read file';
|
||||
error = err instanceof Error ? err.message : "Failed to read file";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -105,14 +116,14 @@
|
|||
(progress) => {
|
||||
classificationProgress = progress;
|
||||
},
|
||||
story.currentStory?.mode ?? 'adventure'
|
||||
story.currentStory?.mode ?? "adventure",
|
||||
);
|
||||
parseResult = {
|
||||
...parseResult,
|
||||
entries: classified,
|
||||
};
|
||||
} catch (err) {
|
||||
error = err instanceof Error ? err.message : 'Classification failed';
|
||||
error = err instanceof Error ? err.message : "Classification failed";
|
||||
} finally {
|
||||
classifying = false;
|
||||
}
|
||||
|
|
@ -135,13 +146,13 @@
|
|||
(progress) => {
|
||||
classificationProgress = progress;
|
||||
},
|
||||
story.currentStory?.mode ?? 'adventure'
|
||||
story.currentStory?.mode ?? "adventure",
|
||||
);
|
||||
classifying = false;
|
||||
}
|
||||
|
||||
// Convert to Entry format
|
||||
const entries = convertToEntries(entriesToImport, 'import');
|
||||
const entries = convertToEntries(entriesToImport, "import");
|
||||
|
||||
// Add each entry to the database
|
||||
const storyId = story.currentStory.id;
|
||||
|
|
@ -161,7 +172,7 @@
|
|||
// Close modal
|
||||
ui.closeLorebookImport();
|
||||
} catch (err) {
|
||||
error = err instanceof Error ? err.message : 'Import failed';
|
||||
error = err instanceof Error ? err.message : "Import failed";
|
||||
} finally {
|
||||
importing = false;
|
||||
classifying = false;
|
||||
|
|
@ -181,7 +192,7 @@
|
|||
<div
|
||||
class="fixed inset-0 z-50 flex items-end sm:items-center justify-center bg-black/60"
|
||||
onclick={close}
|
||||
onkeydown={(e) => e.key === 'Escape' && close()}
|
||||
onkeydown={(e) => e.key === "Escape" && close()}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
tabindex="-1"
|
||||
|
|
@ -199,7 +210,9 @@
|
|||
</div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between border-b border-surface-700 pb-4 pt-0 sm:pt-0">
|
||||
<div
|
||||
class="flex items-center justify-between border-b border-surface-700 pb-4 pt-0 sm:pt-0"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<Upload class="h-5 w-5 text-accent-400" />
|
||||
<h2 class="text-xl font-semibold text-surface-100">Import Lorebook</h2>
|
||||
|
|
@ -212,7 +225,9 @@
|
|||
<!-- Content -->
|
||||
<div class="py-4 space-y-4 max-h-[60vh] overflow-y-auto">
|
||||
{#if error}
|
||||
<div class="p-3 rounded-lg bg-red-500/20 border border-red-500/50 flex items-start gap-2">
|
||||
<div
|
||||
class="p-3 rounded-lg bg-red-500/20 border border-red-500/50 flex items-start gap-2"
|
||||
>
|
||||
<AlertCircle class="h-5 w-5 text-red-400 flex-shrink-0 mt-0.5" />
|
||||
<p class="text-red-300 text-sm">{error}</p>
|
||||
</div>
|
||||
|
|
@ -222,14 +237,16 @@
|
|||
<!-- File upload area -->
|
||||
<div
|
||||
class="border-2 border-dashed rounded-lg p-8 text-center transition-colors
|
||||
{dragOver ? 'border-accent-500 bg-accent-500/10' : 'border-surface-600 hover:border-surface-500'}"
|
||||
{dragOver
|
||||
? 'border-accent-500 bg-accent-500/10'
|
||||
: 'border-surface-600 hover:border-surface-500'}"
|
||||
ondrop={handleDrop}
|
||||
ondragover={handleDragOver}
|
||||
ondragleave={handleDragLeave}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
onclick={() => fileInput.click()}
|
||||
onkeydown={(e) => e.key === 'Enter' && fileInput.click()}
|
||||
onkeydown={(e) => e.key === "Enter" && fileInput.click()}
|
||||
>
|
||||
<FileJson class="h-12 w-12 text-surface-500 mx-auto mb-3" />
|
||||
<p class="text-surface-300 mb-1">Drop a lorebook file here</p>
|
||||
|
|
@ -244,7 +261,7 @@
|
|||
/>
|
||||
|
||||
<p class="text-xs text-surface-500">
|
||||
Supports Aventura (.avt, .json) and SillyTavern lorebook formats
|
||||
Supports Aventuras (.avt, .json) and SillyTavern lorebook formats
|
||||
</p>
|
||||
{:else}
|
||||
<!-- Preview -->
|
||||
|
|
@ -266,10 +283,14 @@
|
|||
</div>
|
||||
|
||||
<!-- AI Classification toggle -->
|
||||
<label class="flex items-center justify-between p-3 rounded-lg bg-surface-800/50 border border-surface-700 cursor-pointer">
|
||||
<label
|
||||
class="flex items-center justify-between p-3 rounded-lg bg-surface-800/50 border border-surface-700 cursor-pointer"
|
||||
>
|
||||
<div>
|
||||
<div class="text-surface-200">AI-powered classification</div>
|
||||
<div class="text-xs text-surface-500">Use AI to better categorize entry types</div>
|
||||
<div class="text-xs text-surface-500">
|
||||
Use AI to better categorize entry types
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
|
|
@ -279,10 +300,14 @@
|
|||
</label>
|
||||
|
||||
{#if classifying}
|
||||
<div class="p-3 rounded-lg bg-surface-800/50 border border-surface-700">
|
||||
<div
|
||||
class="p-3 rounded-lg bg-surface-800/50 border border-surface-700"
|
||||
>
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<Loader2 class="h-4 w-4 text-accent-400 animate-spin" />
|
||||
<span class="text-sm text-surface-300">Classifying entries...</span>
|
||||
<span class="text-sm text-surface-300"
|
||||
>Classifying entries...</span
|
||||
>
|
||||
</div>
|
||||
<div class="w-full h-2 bg-surface-700 rounded-full overflow-hidden">
|
||||
<div
|
||||
|
|
@ -296,7 +321,10 @@
|
|||
<!-- Change file button -->
|
||||
<button
|
||||
class="text-sm text-accent-400 hover:text-accent-300"
|
||||
onclick={() => { parseResult = null; error = null; }}
|
||||
onclick={() => {
|
||||
parseResult = null;
|
||||
error = null;
|
||||
}}
|
||||
>
|
||||
Choose a different file
|
||||
</button>
|
||||
|
|
@ -319,7 +347,7 @@
|
|||
>
|
||||
{#if importing || classifying}
|
||||
<Loader2 class="h-4 w-4 animate-spin" />
|
||||
{classifying ? 'Classifying...' : 'Importing...'}
|
||||
{classifying ? "Classifying..." : "Importing..."}
|
||||
{:else}
|
||||
Import {previewCount} Entries
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -503,7 +503,8 @@
|
|||
placeholder={'{"temperature": 0.7, "top_p": 0.9}'}
|
||||
></textarea>
|
||||
<p class="text-xs text-surface-500 mt-1">
|
||||
Overrides request parameters; messages and tools are managed by Aventura.
|
||||
Overrides request parameters; messages and tools are managed by
|
||||
Aventuras.
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,28 @@
|
|||
<script lang="ts">
|
||||
import { settings } from '$lib/stores/settings.svelte';
|
||||
import { Cpu, RefreshCw } from 'lucide-svelte';
|
||||
import ProviderOnlySelector from './ProviderOnlySelector.svelte';
|
||||
import type { ProviderInfo } from '$lib/services/ai/types';
|
||||
import { OpenAIProvider } from '$lib/services/ai/openrouter';
|
||||
import type { ReasoningEffort } from '$lib/types';
|
||||
import { settings } from "$lib/stores/settings.svelte";
|
||||
import { Cpu, RefreshCw } from "lucide-svelte";
|
||||
import ProviderOnlySelector from "./ProviderOnlySelector.svelte";
|
||||
import type { ProviderInfo } from "$lib/services/ai/types";
|
||||
import { OpenAIProvider } from "$lib/services/ai/openrouter";
|
||||
import type { ReasoningEffort } from "$lib/types";
|
||||
|
||||
interface Props {
|
||||
providerOptions: ProviderInfo[];
|
||||
onOpenManualBodyEditor: (title: string, value: string, onSave: (next: string) => void) => void;
|
||||
onOpenManualBodyEditor: (
|
||||
title: string,
|
||||
value: string,
|
||||
onSave: (next: string) => void,
|
||||
) => void;
|
||||
}
|
||||
|
||||
let { providerOptions, onOpenManualBodyEditor }: Props = $props();
|
||||
|
||||
const reasoningLevels: ReasoningEffort[] = ['off', 'low', 'medium', 'high'];
|
||||
const reasoningLevels: ReasoningEffort[] = ["off", "low", "medium", "high"];
|
||||
const reasoningLabels: Record<ReasoningEffort, string> = {
|
||||
off: 'Off',
|
||||
low: 'Low',
|
||||
medium: 'Medium',
|
||||
high: 'High',
|
||||
off: "Off",
|
||||
low: "Low",
|
||||
medium: "Medium",
|
||||
high: "High",
|
||||
};
|
||||
|
||||
let isLoadingModels = $state(false);
|
||||
|
|
@ -28,21 +32,23 @@
|
|||
let profileModels = $derived.by(() => {
|
||||
const profile = settings.getMainNarrativeProfile();
|
||||
if (!profile) return [];
|
||||
const models = [...new Set([...profile.fetchedModels, ...profile.customModels])];
|
||||
|
||||
const models = [
|
||||
...new Set([...profile.fetchedModels, ...profile.customModels]),
|
||||
];
|
||||
|
||||
const providerPriority: Record<string, number> = {
|
||||
'x-ai': 1,
|
||||
'deepseek': 2,
|
||||
'openai': 3,
|
||||
'anthropic': 4,
|
||||
'google': 5,
|
||||
'meta-llama': 6,
|
||||
'mistralai': 7,
|
||||
"x-ai": 1,
|
||||
deepseek: 2,
|
||||
openai: 3,
|
||||
anthropic: 4,
|
||||
google: 5,
|
||||
"meta-llama": 6,
|
||||
mistralai: 7,
|
||||
};
|
||||
|
||||
return models.sort((a, b) => {
|
||||
const providerA = a.split('/')[0];
|
||||
const providerB = b.split('/')[0];
|
||||
const providerA = a.split("/")[0];
|
||||
const providerB = b.split("/")[0];
|
||||
const priorityA = providerPriority[providerA] ?? 99;
|
||||
const priorityB = providerPriority[providerB] ?? 99;
|
||||
|
||||
|
|
@ -52,7 +58,7 @@
|
|||
});
|
||||
|
||||
function getReasoningIndex(value?: ReasoningEffort): number {
|
||||
const index = reasoningLevels.indexOf(value ?? 'off');
|
||||
const index = reasoningLevels.indexOf(value ?? "off");
|
||||
return index === -1 ? 0 : index;
|
||||
}
|
||||
|
||||
|
|
@ -79,23 +85,31 @@
|
|||
const fetchedModels = await provider.listModels();
|
||||
|
||||
const filteredModelIds = fetchedModels
|
||||
.filter(m => {
|
||||
.filter((m) => {
|
||||
const id = m.id.toLowerCase();
|
||||
if (id.includes('embedding') || id.includes('vision-only') || id.includes('tts') || id.includes('whisper')) {
|
||||
if (
|
||||
id.includes("embedding") ||
|
||||
id.includes("vision-only") ||
|
||||
id.includes("tts") ||
|
||||
id.includes("whisper")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map(m => m.id);
|
||||
.map((m) => m.id);
|
||||
|
||||
await settings.updateProfile(profile.id, {
|
||||
fetchedModels: filteredModelIds,
|
||||
});
|
||||
|
||||
console.log(`[MainNarrative] Fetched ${filteredModelIds.length} models to profile`);
|
||||
console.log(
|
||||
`[MainNarrative] Fetched ${filteredModelIds.length} models to profile`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('[MainNarrative] Failed to fetch models:', error);
|
||||
modelError = error instanceof Error ? error.message : 'Failed to load models.';
|
||||
console.error("[MainNarrative] Failed to fetch models:", error);
|
||||
modelError =
|
||||
error instanceof Error ? error.message : "Failed to load models.";
|
||||
} finally {
|
||||
isLoadingModels = false;
|
||||
}
|
||||
|
|
@ -107,7 +121,7 @@
|
|||
<Cpu class="h-5 w-5 text-accent-400" />
|
||||
<h3 class="text-sm font-semibold text-surface-100">Main Narrative</h3>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- API Endpoint -->
|
||||
<div>
|
||||
|
|
@ -122,7 +136,8 @@
|
|||
{#each settings.apiSettings.profiles as profile (profile.id)}
|
||||
<option value={profile.id}>
|
||||
{profile.name}
|
||||
{#if profile.id === settings.getDefaultProfileIdForProvider()} (Default){/if}
|
||||
{#if profile.id === settings.getDefaultProfileIdForProvider()}
|
||||
(Default){/if}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
|
|
@ -131,21 +146,19 @@
|
|||
<!-- Model Select -->
|
||||
<div>
|
||||
<div class="mb-1.5 flex items-center justify-between">
|
||||
<label class="text-xs font-medium text-surface-400">
|
||||
Model
|
||||
</label>
|
||||
<label class="text-xs font-medium text-surface-400"> Model </label>
|
||||
<button
|
||||
class="flex items-center gap-1 text-xs text-accent-400 hover:text-accent-300 disabled:opacity-50"
|
||||
onclick={fetchModelsToProfile}
|
||||
disabled={isLoadingModels}
|
||||
>
|
||||
<span class={isLoadingModels ? 'animate-spin' : ''}>
|
||||
<span class={isLoadingModels ? "animate-spin" : ""}>
|
||||
<RefreshCw class="h-3 w-3" />
|
||||
</span>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
{#if modelError}
|
||||
<p class="mb-1 text-xs text-amber-400">{modelError}</p>
|
||||
{/if}
|
||||
|
|
@ -175,7 +188,10 @@
|
|||
</div>
|
||||
|
||||
<!-- Temperature & Max Tokens Row -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4 pt-4 border-t border-surface-700" class:opacity-50={settings.advancedRequestSettings.manualMode}>
|
||||
<div
|
||||
class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4 pt-4 border-t border-surface-700"
|
||||
class:opacity-50={settings.advancedRequestSettings.manualMode}
|
||||
>
|
||||
<div>
|
||||
<label class="mb-1.5 block text-xs font-medium text-surface-400">
|
||||
Temperature: {settings.apiSettings.temperature.toFixed(1)}
|
||||
|
|
@ -186,7 +202,8 @@
|
|||
max="1"
|
||||
step="0.1"
|
||||
value={settings.apiSettings.temperature}
|
||||
oninput={(e) => settings.setTemperature(parseFloat(e.currentTarget.value))}
|
||||
oninput={(e) =>
|
||||
settings.setTemperature(parseFloat(e.currentTarget.value))}
|
||||
disabled={settings.advancedRequestSettings.manualMode}
|
||||
class="w-full"
|
||||
/>
|
||||
|
|
@ -229,7 +246,10 @@
|
|||
</div>
|
||||
|
||||
<!-- Thinking & Provider Row -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4 pt-4 border-t border-surface-700" class:opacity-50={settings.advancedRequestSettings.manualMode}>
|
||||
<div
|
||||
class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4 pt-4 border-t border-surface-700"
|
||||
class:opacity-50={settings.advancedRequestSettings.manualMode}
|
||||
>
|
||||
<div>
|
||||
<label class="mb-1.5 block text-xs font-medium text-surface-400">
|
||||
Thinking: {reasoningLabels[settings.apiSettings.reasoningEffort]}
|
||||
|
|
@ -240,7 +260,10 @@
|
|||
max="3"
|
||||
step="1"
|
||||
value={getReasoningIndex(settings.apiSettings.reasoningEffort)}
|
||||
onchange={(e) => settings.setMainReasoningEffort(getReasoningValue(parseInt(e.currentTarget.value)))}
|
||||
onchange={(e) =>
|
||||
settings.setMainReasoningEffort(
|
||||
getReasoningValue(parseInt(e.currentTarget.value)),
|
||||
)}
|
||||
disabled={settings.advancedRequestSettings.manualMode}
|
||||
class="w-full"
|
||||
/>
|
||||
|
|
@ -267,25 +290,34 @@
|
|||
{#if settings.advancedRequestSettings.manualMode}
|
||||
<div class="mt-4 pt-4 border-t border-surface-700">
|
||||
<div class="mb-1 flex items-center justify-between">
|
||||
<label class="text-xs font-medium text-surface-400">Manual Request Body (JSON)</label>
|
||||
<label class="text-xs font-medium text-surface-400"
|
||||
>Manual Request Body (JSON)</label
|
||||
>
|
||||
<button
|
||||
class="text-xs text-accent-400 hover:text-accent-300"
|
||||
onclick={() => onOpenManualBodyEditor('Main Narrative', settings.apiSettings.manualBody, (next) => {
|
||||
settings.apiSettings.manualBody = next;
|
||||
settings.setMainManualBody(next);
|
||||
})}
|
||||
onclick={() =>
|
||||
onOpenManualBodyEditor(
|
||||
"Main Narrative",
|
||||
settings.apiSettings.manualBody,
|
||||
(next) => {
|
||||
settings.apiSettings.manualBody = next;
|
||||
settings.setMainManualBody(next);
|
||||
},
|
||||
)}
|
||||
>
|
||||
Pop out
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
bind:value={settings.apiSettings.manualBody}
|
||||
onblur={() => settings.setMainManualBody(settings.apiSettings.manualBody)}
|
||||
onblur={() =>
|
||||
settings.setMainManualBody(settings.apiSettings.manualBody)}
|
||||
class="input text-xs min-h-[100px] resize-y font-mono w-full"
|
||||
rows="4"
|
||||
></textarea>
|
||||
<p class="text-xs text-surface-500 mt-1">
|
||||
Overrides request parameters; messages and tools are managed by Aventura.
|
||||
Overrides request parameters; messages and tools are managed by
|
||||
Aventuras.
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
<script lang="ts">
|
||||
import { settings } from '$lib/stores/settings.svelte';
|
||||
import { promptExportService } from '$lib/services/promptExport';
|
||||
import { settings } from "$lib/stores/settings.svelte";
|
||||
import { promptExportService } from "$lib/services/promptExport";
|
||||
import type {
|
||||
ParsedPromptImport,
|
||||
ImportPresetConfig,
|
||||
ReasoningEffort,
|
||||
} from '$lib/types';
|
||||
} from "$lib/types";
|
||||
import {
|
||||
X,
|
||||
Upload,
|
||||
|
|
@ -16,10 +16,10 @@
|
|||
AlertTriangle,
|
||||
ChevronDown,
|
||||
Sparkles,
|
||||
} from 'lucide-svelte';
|
||||
import { swipe } from '$lib/utils/swipe';
|
||||
import { fly, fade, slide } from 'svelte/transition';
|
||||
import CompactSelect from './CompactSelect.svelte';
|
||||
} from "lucide-svelte";
|
||||
import { swipe } from "$lib/utils/swipe";
|
||||
import { fly, fade, slide } from "svelte/transition";
|
||||
import CompactSelect from "./CompactSelect.svelte";
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
|
|
@ -41,11 +41,13 @@
|
|||
|
||||
// Derived
|
||||
const availableProfiles = $derived(
|
||||
settings.apiSettings.profiles.map(p => ({ id: p.id, name: p.name }))
|
||||
settings.apiSettings.profiles.map((p) => ({ id: p.id, name: p.name })),
|
||||
);
|
||||
|
||||
function getAvailableModelsForProfile(profileId: string): string[] {
|
||||
const profile = settings.apiSettings.profiles.find(p => p.id === profileId);
|
||||
const profile = settings.apiSettings.profiles.find(
|
||||
(p) => p.id === profileId,
|
||||
);
|
||||
if (!profile) return [];
|
||||
return [...profile.customModels, ...profile.fetchedModels];
|
||||
}
|
||||
|
|
@ -61,7 +63,7 @@
|
|||
};
|
||||
});
|
||||
|
||||
const stepTitles = ['Select File', 'Configure', 'Import'];
|
||||
const stepTitles = ["Select File", "Configure", "Import"];
|
||||
|
||||
// Functions
|
||||
function resetState() {
|
||||
|
|
@ -105,8 +107,8 @@
|
|||
error = null;
|
||||
parseResult = null;
|
||||
|
||||
if (!file.name.endsWith('.json')) {
|
||||
error = 'Please select a JSON file';
|
||||
if (!file.name.endsWith(".json")) {
|
||||
error = "Please select a JSON file";
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -115,7 +117,7 @@
|
|||
const result = promptExportService.parseImportFile(text);
|
||||
|
||||
if (!result.success) {
|
||||
error = result.errors.join(', ');
|
||||
error = result.errors.join(", ");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -123,7 +125,7 @@
|
|||
|
||||
const configs = new Map<string, ImportPresetConfig>();
|
||||
const profiles = settings.apiSettings.profiles;
|
||||
const defaultProfileId = profiles.length > 0 ? profiles[0].id : '';
|
||||
const defaultProfileId = profiles.length > 0 ? profiles[0].id : "";
|
||||
|
||||
for (const preset of result.data!.generationPresets) {
|
||||
configs.set(preset.id, {
|
||||
|
|
@ -141,11 +143,14 @@
|
|||
presetConfigs = configs;
|
||||
currentStep = 2;
|
||||
} catch (err) {
|
||||
error = err instanceof Error ? err.message : 'Failed to read file';
|
||||
error = err instanceof Error ? err.message : "Failed to read file";
|
||||
}
|
||||
}
|
||||
|
||||
function updatePresetConfig(presetId: string, updates: Partial<ImportPresetConfig>) {
|
||||
function updatePresetConfig(
|
||||
presetId: string,
|
||||
updates: Partial<ImportPresetConfig>,
|
||||
) {
|
||||
const newConfigs = new Map(presetConfigs);
|
||||
const config = newConfigs.get(presetId)!;
|
||||
newConfigs.set(presetId, { ...config, ...updates });
|
||||
|
|
@ -171,7 +176,7 @@
|
|||
handleClose();
|
||||
}, 2000);
|
||||
} catch (err) {
|
||||
error = err instanceof Error ? err.message : 'Import failed';
|
||||
error = err instanceof Error ? err.message : "Import failed";
|
||||
} finally {
|
||||
isImporting = false;
|
||||
}
|
||||
|
|
@ -183,7 +188,7 @@
|
|||
<div
|
||||
class="fixed inset-0 z-50 flex items-end sm:items-center justify-center"
|
||||
onclick={handleClose}
|
||||
onkeydown={(e) => e.key === 'Escape' && handleClose()}
|
||||
onkeydown={(e) => e.key === "Escape" && handleClose()}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
tabindex="-1"
|
||||
|
|
@ -215,8 +220,12 @@
|
|||
<Upload class="h-5 w-5 text-accent-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-surface-100">Import Prompts</h2>
|
||||
<p class="text-xs text-surface-500">{stepTitles[currentStep - 1]}</p>
|
||||
<h2 class="text-lg font-semibold text-surface-100">
|
||||
Import Prompts
|
||||
</h2>
|
||||
<p class="text-xs text-surface-500">
|
||||
{stepTitles[currentStep - 1]}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
|
|
@ -232,10 +241,12 @@
|
|||
{#each [1, 2, 3] as step}
|
||||
<div class="flex-1 flex items-center gap-2">
|
||||
<div
|
||||
class="h-1.5 flex-1 rounded-full transition-all duration-300 {
|
||||
step < currentStep ? 'bg-accent-500' :
|
||||
step === currentStep ? 'bg-accent-500' : 'bg-surface-700'
|
||||
}"
|
||||
class="h-1.5 flex-1 rounded-full transition-all duration-300 {step <
|
||||
currentStep
|
||||
? 'bg-accent-500'
|
||||
: step === currentStep
|
||||
? 'bg-accent-500'
|
||||
: 'bg-surface-700'}"
|
||||
></div>
|
||||
</div>
|
||||
{/each}
|
||||
|
|
@ -246,7 +257,10 @@
|
|||
<div class="flex-1 overflow-y-auto px-5 sm:px-6 pb-4">
|
||||
<!-- Error Message -->
|
||||
{#if error}
|
||||
<div class="mb-4 p-3 rounded-xl bg-red-500/10 border border-red-500/20 flex items-start gap-3" transition:fly={{ y: -10, duration: 200 }}>
|
||||
<div
|
||||
class="mb-4 p-3 rounded-xl bg-red-500/10 border border-red-500/20 flex items-start gap-3"
|
||||
transition:fly={{ y: -10, duration: 200 }}
|
||||
>
|
||||
<AlertCircle class="h-5 w-5 text-red-400 flex-shrink-0 mt-0.5" />
|
||||
<p class="text-red-300 text-sm">{error}</p>
|
||||
</div>
|
||||
|
|
@ -254,8 +268,12 @@
|
|||
|
||||
<!-- Warnings -->
|
||||
{#if parseResult?.warnings && parseResult.warnings.length > 0}
|
||||
<div class="mb-4 p-3 rounded-xl bg-amber-500/10 border border-amber-500/20 flex items-start gap-3">
|
||||
<AlertTriangle class="h-5 w-5 text-amber-400 flex-shrink-0 mt-0.5" />
|
||||
<div
|
||||
class="mb-4 p-3 rounded-xl bg-amber-500/10 border border-amber-500/20 flex items-start gap-3"
|
||||
>
|
||||
<AlertTriangle
|
||||
class="h-5 w-5 text-amber-400 flex-shrink-0 mt-0.5"
|
||||
/>
|
||||
<div class="text-amber-300 text-sm">
|
||||
{#each parseResult.warnings as warning}
|
||||
<p>{warning}</p>
|
||||
|
|
@ -268,23 +286,23 @@
|
|||
{#if currentStep === 1}
|
||||
<div class="space-y-4" transition:fade={{ duration: 150 }}>
|
||||
<div
|
||||
class="border-2 border-dashed rounded-2xl p-8 sm:p-10 text-center transition-all cursor-pointer {
|
||||
dragOver
|
||||
? 'border-accent-500 bg-accent-500/5 scale-[1.02]'
|
||||
: 'border-surface-600 hover:border-surface-500 hover:bg-surface-800/30'
|
||||
}"
|
||||
class="border-2 border-dashed rounded-2xl p-8 sm:p-10 text-center transition-all cursor-pointer {dragOver
|
||||
? 'border-accent-500 bg-accent-500/5 scale-[1.02]'
|
||||
: 'border-surface-600 hover:border-surface-500 hover:bg-surface-800/30'}"
|
||||
ondrop={handleDrop}
|
||||
ondragover={handleDragOver}
|
||||
ondragleave={handleDragLeave}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
onclick={() => fileInput.click()}
|
||||
onkeydown={(e) => e.key === 'Enter' && fileInput.click()}
|
||||
onkeydown={(e) => e.key === "Enter" && fileInput.click()}
|
||||
>
|
||||
<div class="inline-flex p-4 rounded-2xl bg-surface-800/50 mb-4">
|
||||
<FileJson class="h-10 w-10 text-surface-400" />
|
||||
</div>
|
||||
<p class="text-surface-200 font-medium mb-1">Drop your file here</p>
|
||||
<p class="text-surface-200 font-medium mb-1">
|
||||
Drop your file here
|
||||
</p>
|
||||
<p class="text-sm text-surface-500">or click to browse</p>
|
||||
</div>
|
||||
<input
|
||||
|
|
@ -295,30 +313,38 @@
|
|||
onchange={handleFileSelect}
|
||||
/>
|
||||
<p class="text-center text-xs text-surface-500">
|
||||
Supports Aventura prompt export files (.json)
|
||||
Supports Aventuras prompt export files (.json)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: Configure Presets -->
|
||||
<!-- Step 2: Configure Presets -->
|
||||
{:else if currentStep === 2 && parseResult?.data}
|
||||
<div class="space-y-4" transition:fade={{ duration: 150 }}>
|
||||
<!-- Stats Summary -->
|
||||
{#if importStats}
|
||||
<div class="grid grid-cols-2 sm:grid-cols-4 gap-2">
|
||||
<div class="p-3 rounded-xl bg-surface-800/50 text-center">
|
||||
<div class="text-lg font-semibold text-surface-100">{importStats.templateOverrides}</div>
|
||||
<div class="text-lg font-semibold text-surface-100">
|
||||
{importStats.templateOverrides}
|
||||
</div>
|
||||
<div class="text-xs text-surface-500">Prompts</div>
|
||||
</div>
|
||||
<div class="p-3 rounded-xl bg-surface-800/50 text-center">
|
||||
<div class="text-lg font-semibold text-surface-100">{importStats.customMacros}</div>
|
||||
<div class="text-lg font-semibold text-surface-100">
|
||||
{importStats.customMacros}
|
||||
</div>
|
||||
<div class="text-xs text-surface-500">Macros</div>
|
||||
</div>
|
||||
<div class="p-3 rounded-xl bg-surface-800/50 text-center">
|
||||
<div class="text-lg font-semibold text-surface-100">{importStats.macroOverrides}</div>
|
||||
<div class="text-lg font-semibold text-surface-100">
|
||||
{importStats.macroOverrides}
|
||||
</div>
|
||||
<div class="text-xs text-surface-500">Overrides</div>
|
||||
</div>
|
||||
<div class="p-3 rounded-xl bg-surface-800/50 text-center">
|
||||
<div class="text-lg font-semibold text-surface-100">{importStats.presets}</div>
|
||||
<div class="text-lg font-semibold text-surface-100">
|
||||
{importStats.presets}
|
||||
</div>
|
||||
<div class="text-xs text-surface-500">Presets</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -327,50 +353,78 @@
|
|||
<!-- Presets Configuration -->
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h3 class="text-sm font-medium text-surface-300">Generation Presets</h3>
|
||||
<h3 class="text-sm font-medium text-surface-300">
|
||||
Generation Presets
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
{#each parseResult.data.generationPresets as preset (preset.id)}
|
||||
{@const config = presetConfigs.get(preset.id)!}
|
||||
{@const isExpanded = expandedPreset === preset.id}
|
||||
{@const availableModels = getAvailableModelsForProfile(config.profileId)}
|
||||
{@const availableModels = getAvailableModelsForProfile(
|
||||
config.profileId,
|
||||
)}
|
||||
|
||||
<div class="rounded-xl border border-surface-700 bg-surface-800/30 transition-all">
|
||||
<div
|
||||
class="rounded-xl border border-surface-700 bg-surface-800/30 transition-all"
|
||||
>
|
||||
<!-- Preset Header with Profile & Model (always visible) -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<div
|
||||
class="w-full px-3 py-2.5 flex flex-col gap-2 text-left hover:bg-surface-800/50 transition-colors cursor-pointer rounded-t-xl {isExpanded ? '' : 'rounded-b-xl'} focus:outline-none"
|
||||
class="w-full px-3 py-2.5 flex flex-col gap-2 text-left hover:bg-surface-800/50 transition-colors cursor-pointer rounded-t-xl {isExpanded
|
||||
? ''
|
||||
: 'rounded-b-xl'} focus:outline-none"
|
||||
onclick={() => togglePresetExpanded(preset.id)}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<!-- Header Row (Icon + Name + Original Model) -->
|
||||
<div class="flex items-center gap-3 w-full min-w-0">
|
||||
<Sparkles class="h-4 w-4 text-accent-400 flex-shrink-0" />
|
||||
<div class="flex-1 min-w-0 flex items-center gap-2 overflow-hidden">
|
||||
<span class="text-sm font-medium text-surface-100 truncate">{preset.name}</span>
|
||||
<span class="text-xs text-surface-500 truncate border-l border-surface-700 pl-2">
|
||||
Was: <span class="text-surface-400">{preset.model}</span>
|
||||
<Sparkles
|
||||
class="h-4 w-4 text-accent-400 flex-shrink-0"
|
||||
/>
|
||||
<div
|
||||
class="flex-1 min-w-0 flex items-center gap-2 overflow-hidden"
|
||||
>
|
||||
<span
|
||||
class="text-sm font-medium text-surface-100 truncate"
|
||||
>{preset.name}</span
|
||||
>
|
||||
<span
|
||||
class="text-xs text-surface-500 truncate border-l border-surface-700 pl-2"
|
||||
>
|
||||
Was: <span class="text-surface-400"
|
||||
>{preset.model}</span
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
<ChevronDown class="h-4 w-4 text-surface-400 transition-transform flex-shrink-0 {isExpanded ? 'rotate-180' : ''}" />
|
||||
<ChevronDown
|
||||
class="h-4 w-4 text-surface-400 transition-transform flex-shrink-0 {isExpanded
|
||||
? 'rotate-180'
|
||||
: ''}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Controls Row -->
|
||||
<div class="flex items-center gap-2 w-full">
|
||||
<!-- Profile Select -->
|
||||
<CompactSelect
|
||||
value={config.profileId}
|
||||
options={availableProfiles.map(p => ({ value: p.id, label: p.name }))}
|
||||
options={availableProfiles.map((p) => ({
|
||||
value: p.id,
|
||||
label: p.name,
|
||||
}))}
|
||||
class="flex-1"
|
||||
onSelect={(newProfileId) => {
|
||||
if (!newProfileId) return;
|
||||
const models = getAvailableModelsForProfile(newProfileId);
|
||||
const newModel = models.length > 0 ? models[0] : config.model;
|
||||
const models =
|
||||
getAvailableModelsForProfile(newProfileId);
|
||||
const newModel =
|
||||
models.length > 0 ? models[0] : config.model;
|
||||
updatePresetConfig(preset.id, {
|
||||
profileId: newProfileId,
|
||||
model: newModel
|
||||
model: newModel,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
|
@ -379,14 +433,18 @@
|
|||
<CompactSelect
|
||||
value={config.model}
|
||||
options={availableModels.length > 0
|
||||
? availableModels.map(m => ({ value: m, label: m }))
|
||||
: [{ value: null, label: 'No models' }]
|
||||
}
|
||||
? availableModels.map((m) => ({
|
||||
value: m,
|
||||
label: m,
|
||||
}))
|
||||
: [{ value: null, label: "No models" }]}
|
||||
class="flex-1"
|
||||
placeholder="Select model"
|
||||
onSelect={(newModel) => {
|
||||
if (newModel) {
|
||||
updatePresetConfig(preset.id, { model: newModel });
|
||||
updatePresetConfig(preset.id, {
|
||||
model: newModel,
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
|
@ -395,12 +453,16 @@
|
|||
|
||||
<!-- Expanded Settings -->
|
||||
{#if isExpanded}
|
||||
<div class="px-3 pb-3 pt-2 border-t border-surface-700/30" transition:slide={{ duration: 200 }}>
|
||||
<div
|
||||
class="px-3 pb-3 pt-2 border-t border-surface-700/30"
|
||||
transition:slide={{ duration: 200 }}
|
||||
>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
|
||||
<!-- Temperature -->
|
||||
<div>
|
||||
<label class="text-xs text-surface-500 mb-1 block">Temperature</label>
|
||||
<label class="text-xs text-surface-500 mb-1 block"
|
||||
>Temperature</label
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
step="0.1"
|
||||
|
|
@ -408,36 +470,51 @@
|
|||
max="2"
|
||||
class="w-full px-2 py-1.5 rounded-md bg-surface-700/50 border border-surface-600 text-surface-100 text-sm focus:border-accent-500 focus:ring-1 focus:ring-accent-500 transition-colors"
|
||||
value={config.temperature}
|
||||
oninput={(e) => updatePresetConfig(preset.id, { temperature: parseFloat(e.currentTarget.value) || 0 })}
|
||||
oninput={(e) =>
|
||||
updatePresetConfig(preset.id, {
|
||||
temperature:
|
||||
parseFloat(e.currentTarget.value) || 0,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Max Tokens -->
|
||||
<div>
|
||||
<label class="text-xs text-surface-500 mb-1 block">Max Tokens</label>
|
||||
<label class="text-xs text-surface-500 mb-1 block"
|
||||
>Max Tokens</label
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
class="w-full px-2 py-1.5 rounded-md bg-surface-700/50 border border-surface-600 text-surface-100 text-sm focus:border-accent-500 focus:ring-1 focus:ring-accent-500 transition-colors"
|
||||
value={config.maxTokens}
|
||||
oninput={(e) => updatePresetConfig(preset.id, { maxTokens: parseInt(e.currentTarget.value) || 1 })}
|
||||
oninput={(e) =>
|
||||
updatePresetConfig(preset.id, {
|
||||
maxTokens:
|
||||
parseInt(e.currentTarget.value) || 1,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Reasoning Effort -->
|
||||
<div class="col-span-2">
|
||||
<label class="text-xs text-surface-500 mb-1 block">Reasoning</label>
|
||||
<label class="text-xs text-surface-500 mb-1 block"
|
||||
>Reasoning</label
|
||||
>
|
||||
<div class="grid grid-cols-4 gap-1">
|
||||
{#each ['off', 'low', 'medium', 'high'] as level}
|
||||
{#each ["off", "low", "medium", "high"] as level}
|
||||
<button
|
||||
class="py-1.5 px-2 rounded-md text-xs font-medium transition-colors {
|
||||
config.reasoningEffort === level
|
||||
? 'bg-accent-500 text-white'
|
||||
: 'bg-surface-700/50 text-surface-400 hover:bg-surface-700'
|
||||
}"
|
||||
onclick={() => updatePresetConfig(preset.id, { reasoningEffort: level as ReasoningEffort })}
|
||||
class="py-1.5 px-2 rounded-md text-xs font-medium transition-colors {config.reasoningEffort ===
|
||||
level
|
||||
? 'bg-accent-500 text-white'
|
||||
: 'bg-surface-700/50 text-surface-400 hover:bg-surface-700'}"
|
||||
onclick={() =>
|
||||
updatePresetConfig(preset.id, {
|
||||
reasoningEffort: level as ReasoningEffort,
|
||||
})}
|
||||
>
|
||||
{level.charAt(0).toUpperCase() + level.slice(1)}
|
||||
{level.charAt(0).toUpperCase() +
|
||||
level.slice(1)}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
@ -451,29 +528,39 @@
|
|||
</div>
|
||||
|
||||
<!-- Warning -->
|
||||
<div class="p-3 rounded-xl bg-amber-500/5 border border-amber-500/20 flex items-center gap-3">
|
||||
<div
|
||||
class="p-3 rounded-xl bg-amber-500/5 border border-amber-500/20 flex items-center gap-3"
|
||||
>
|
||||
<AlertTriangle class="h-5 w-5 text-amber-400 flex-shrink-0" />
|
||||
<p class="text-xs text-amber-300/80">
|
||||
Importing will <span class="font-medium text-amber-300">replace</span> all current prompts, macros, and presets.
|
||||
Importing will <span class="font-medium text-amber-300"
|
||||
>replace</span
|
||||
> all current prompts, macros, and presets.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 3: Success -->
|
||||
<!-- Step 3: Success -->
|
||||
{:else if currentStep === 3}
|
||||
<div class="py-8 text-center" transition:fade={{ duration: 150 }}>
|
||||
<div class="inline-flex p-4 rounded-full bg-green-500/10 mb-4">
|
||||
<Check class="h-10 w-10 text-green-400" />
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold text-surface-100 mb-2">Import Complete!</h3>
|
||||
<p class="text-surface-400">Your prompts have been imported successfully.</p>
|
||||
<h3 class="text-xl font-semibold text-surface-100 mb-2">
|
||||
Import Complete!
|
||||
</h3>
|
||||
<p class="text-surface-400">
|
||||
Your prompts have been imported successfully.
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
{#if currentStep !== 3}
|
||||
<div class="px-5 sm:px-6 py-4 border-t border-surface-700/50 bg-surface-800/30">
|
||||
<div
|
||||
class="px-5 sm:px-6 py-4 border-t border-surface-700/50 bg-surface-800/30"
|
||||
>
|
||||
<div class="flex gap-3">
|
||||
{#if currentStep === 1}
|
||||
<button
|
||||
|
|
@ -485,7 +572,10 @@
|
|||
{:else if currentStep === 2}
|
||||
<button
|
||||
class="py-2.5 px-4 rounded-xl text-sm font-medium text-surface-300 bg-surface-700/50 hover:bg-surface-700 transition-colors"
|
||||
onclick={() => { currentStep = 1; parseResult = null; }}
|
||||
onclick={() => {
|
||||
currentStep = 1;
|
||||
parseResult = null;
|
||||
}}
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -1,20 +1,17 @@
|
|||
<script lang="ts">
|
||||
import { settings, type ProviderPreset } from '$lib/stores/settings.svelte';
|
||||
import { Check, ExternalLink } from 'lucide-svelte';
|
||||
import { settings, type ProviderPreset } from "$lib/stores/settings.svelte";
|
||||
import { Check, ExternalLink } from "lucide-svelte";
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean;
|
||||
onComplete: () => void;
|
||||
}
|
||||
|
||||
let {
|
||||
isOpen,
|
||||
onComplete,
|
||||
}: Props = $props();
|
||||
let { isOpen, onComplete }: Props = $props();
|
||||
|
||||
// Form state
|
||||
let selectedProvider = $state<ProviderPreset>('openrouter');
|
||||
let apiKey = $state('');
|
||||
let selectedProvider = $state<ProviderPreset>("openrouter");
|
||||
let apiKey = $state("");
|
||||
let showApiKey = $state(false);
|
||||
let isSubmitting = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
|
@ -22,41 +19,43 @@
|
|||
// Provider info
|
||||
const providers = [
|
||||
{
|
||||
id: 'openrouter' as ProviderPreset,
|
||||
name: 'OpenRouter',
|
||||
description: 'Access multiple AI models through a single API.',
|
||||
url: 'https://openrouter.ai',
|
||||
signupUrl: 'https://openrouter.ai/keys',
|
||||
keyPrefix: 'sk-or-',
|
||||
id: "openrouter" as ProviderPreset,
|
||||
name: "OpenRouter",
|
||||
description: "Access multiple AI models through a single API.",
|
||||
url: "https://openrouter.ai",
|
||||
signupUrl: "https://openrouter.ai/keys",
|
||||
keyPrefix: "sk-or-",
|
||||
requiresKey: true,
|
||||
},
|
||||
{
|
||||
id: 'nanogpt' as ProviderPreset,
|
||||
name: 'NanoGPT',
|
||||
description: 'Affordable AI API with access to models like Deepseek and GLM.',
|
||||
url: 'https://nano-gpt.com',
|
||||
signupUrl: 'https://nano-gpt.com/api',
|
||||
keyPrefix: 'sk-nano-',
|
||||
id: "nanogpt" as ProviderPreset,
|
||||
name: "NanoGPT",
|
||||
description:
|
||||
"Affordable AI API with access to models like Deepseek and GLM.",
|
||||
url: "https://nano-gpt.com",
|
||||
signupUrl: "https://nano-gpt.com/api",
|
||||
keyPrefix: "sk-nano-",
|
||||
requiresKey: true,
|
||||
note: 'For thinking models, append :thinking to the model name (e.g., deepseek/deepseek-v3.2:thinking)',
|
||||
note: "For thinking models, append :thinking to the model name (e.g., deepseek/deepseek-v3.2:thinking)",
|
||||
},
|
||||
{
|
||||
id: 'custom' as ProviderPreset,
|
||||
name: 'Custom / Self-Hosted',
|
||||
description: 'Configure your own OpenAI-compatible API endpoint in settings.',
|
||||
url: '',
|
||||
signupUrl: '',
|
||||
keyPrefix: '',
|
||||
id: "custom" as ProviderPreset,
|
||||
name: "Custom / Self-Hosted",
|
||||
description:
|
||||
"Configure your own OpenAI-compatible API endpoint in settings.",
|
||||
url: "",
|
||||
signupUrl: "",
|
||||
keyPrefix: "",
|
||||
requiresKey: false,
|
||||
note: 'You can configure your API endpoint and key later in the Settings → API tab.',
|
||||
note: "You can configure your API endpoint and key later in the Settings → API tab.",
|
||||
},
|
||||
];
|
||||
|
||||
$effect(() => {
|
||||
if (isOpen) {
|
||||
// Reset state when modal opens
|
||||
selectedProvider = 'openrouter';
|
||||
apiKey = '';
|
||||
selectedProvider = "openrouter";
|
||||
apiKey = "";
|
||||
showApiKey = false;
|
||||
isSubmitting = false;
|
||||
error = null;
|
||||
|
|
@ -69,7 +68,7 @@
|
|||
|
||||
async function handleSubmit() {
|
||||
if (requiresApiKey() && !apiKey.trim()) {
|
||||
error = 'Please enter your API key';
|
||||
error = "Please enter your API key";
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -80,21 +79,24 @@
|
|||
await settings.initializeWithProvider(selectedProvider, apiKey.trim());
|
||||
onComplete();
|
||||
} catch (e) {
|
||||
console.error('Failed to initialize with provider:', e);
|
||||
error = e instanceof Error ? e.message : 'Failed to initialize. Please try again.';
|
||||
console.error("Failed to initialize with provider:", e);
|
||||
error =
|
||||
e instanceof Error
|
||||
? e.message
|
||||
: "Failed to initialize. Please try again.";
|
||||
} finally {
|
||||
isSubmitting = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
if (e.key === 'Enter' && apiKey.trim()) {
|
||||
if (e.key === "Enter" && apiKey.trim()) {
|
||||
handleSubmit();
|
||||
}
|
||||
}
|
||||
|
||||
function getSelectedProvider() {
|
||||
return providers.find(p => p.id === selectedProvider);
|
||||
return providers.find((p) => p.id === selectedProvider);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
@ -102,7 +104,7 @@
|
|||
<div class="modal-backdrop">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<h1>Welcome to Aventura</h1>
|
||||
<h1>Welcome to Aventuras</h1>
|
||||
<p class="subtitle">Choose your AI provider to get started</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -114,7 +116,7 @@
|
|||
type="button"
|
||||
class="provider-card"
|
||||
class:selected={selectedProvider === provider.id}
|
||||
onclick={() => selectedProvider = provider.id}
|
||||
onclick={() => (selectedProvider = provider.id)}
|
||||
>
|
||||
<div class="provider-header">
|
||||
<span class="provider-name">{provider.name}</span>
|
||||
|
|
@ -149,18 +151,20 @@
|
|||
<div class="api-key-container">
|
||||
<input
|
||||
id="api-key"
|
||||
type={showApiKey ? 'text' : 'password'}
|
||||
type={showApiKey ? "text" : "password"}
|
||||
class="input"
|
||||
placeholder={getSelectedProvider()?.keyPrefix ? `${getSelectedProvider()?.keyPrefix}...` : 'Enter your API key'}
|
||||
placeholder={getSelectedProvider()?.keyPrefix
|
||||
? `${getSelectedProvider()?.keyPrefix}...`
|
||||
: "Enter your API key"}
|
||||
bind:value={apiKey}
|
||||
onkeydown={handleKeyDown}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="toggle-key-btn"
|
||||
onclick={() => showApiKey = !showApiKey}
|
||||
onclick={() => (showApiKey = !showApiKey)}
|
||||
>
|
||||
{showApiKey ? 'Hide' : 'Show'}
|
||||
{showApiKey ? "Hide" : "Show"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -213,7 +217,9 @@
|
|||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 16px 64px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(255, 255, 255, 0.05);
|
||||
box-shadow:
|
||||
0 16px 64px rgba(0, 0, 0, 0.6),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
|
|
@ -423,7 +429,9 @@
|
|||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile adjustments */
|
||||
|
|
|
|||
|
|
@ -782,7 +782,8 @@
|
|||
const SWIPE_TIMEOUT = 400; // Max time in ms for a valid swipe
|
||||
|
||||
// Elements that should not trigger swipe gestures
|
||||
const INTERACTIVE_SELECTORS = 'input[type="range"], input[type="text"], input[type="number"], textarea, select, button, a';
|
||||
const INTERACTIVE_SELECTORS =
|
||||
'input[type="range"], input[type="text"], input[type="number"], textarea, select, button, a';
|
||||
|
||||
function handleTouchStart(e: TouchEvent) {
|
||||
// Check if touch started on an interactive element
|
||||
|
|
@ -2070,7 +2071,7 @@
|
|||
></textarea>
|
||||
<p class="text-xs text-surface-500 mt-2">
|
||||
Overrides request parameters; messages and tools are managed by
|
||||
Aventura.
|
||||
Aventuras.
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -1,10 +1,19 @@
|
|||
<script lang="ts">
|
||||
import { story } from '$lib/stores/story.svelte';
|
||||
import { ui } from '$lib/stores/ui.svelte';
|
||||
import { exportService } from '$lib/services/export';
|
||||
import { ask } from '@tauri-apps/plugin-dialog';
|
||||
import { BookOpen, Trash2, Clock, Sparkles, Upload, RefreshCw, Archive, Plus } from 'lucide-svelte';
|
||||
import SetupWizard from '../wizard/SetupWizard.svelte';
|
||||
import { story } from "$lib/stores/story.svelte";
|
||||
import { ui } from "$lib/stores/ui.svelte";
|
||||
import { exportService } from "$lib/services/export";
|
||||
import { ask } from "@tauri-apps/plugin-dialog";
|
||||
import {
|
||||
BookOpen,
|
||||
Trash2,
|
||||
Clock,
|
||||
Sparkles,
|
||||
Upload,
|
||||
RefreshCw,
|
||||
Archive,
|
||||
Plus,
|
||||
} from "lucide-svelte";
|
||||
import SetupWizard from "../wizard/SetupWizard.svelte";
|
||||
|
||||
// File input for import (HTML-based for mobile compatibility)
|
||||
let importFileInput: HTMLInputElement;
|
||||
|
|
@ -24,37 +33,47 @@
|
|||
|
||||
async function openStory(storyId: string) {
|
||||
await story.loadStory(storyId);
|
||||
ui.setActivePanel('story');
|
||||
ui.setActivePanel("story");
|
||||
}
|
||||
|
||||
async function deleteStory(storyId: string, event: MouseEvent) {
|
||||
event.stopPropagation();
|
||||
const confirmed = await ask('Are you sure you want to delete this story? This action cannot be undone.', {
|
||||
title: 'Delete Story',
|
||||
kind: 'warning',
|
||||
});
|
||||
const confirmed = await ask(
|
||||
"Are you sure you want to delete this story? This action cannot be undone.",
|
||||
{
|
||||
title: "Delete Story",
|
||||
kind: "warning",
|
||||
},
|
||||
);
|
||||
if (confirmed) {
|
||||
await story.deleteStory(storyId);
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(timestamp: number): string {
|
||||
return new Date(timestamp).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
return new Date(timestamp).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
function getGenreColor(genre: string | null): string {
|
||||
switch (genre) {
|
||||
case 'Fantasy': return 'bg-purple-500/20 text-purple-400';
|
||||
case 'Sci-Fi': return 'bg-cyan-500/20 text-cyan-400';
|
||||
case 'Mystery': return 'bg-amber-500/20 text-amber-400';
|
||||
case 'Horror': return 'bg-red-500/20 text-red-400';
|
||||
case 'Slice of Life': return 'bg-green-500/20 text-green-400';
|
||||
case 'Historical': return 'bg-orange-500/20 text-orange-400';
|
||||
default: return 'bg-surface-700 text-surface-400';
|
||||
case "Fantasy":
|
||||
return "bg-purple-500/20 text-purple-400";
|
||||
case "Sci-Fi":
|
||||
return "bg-cyan-500/20 text-cyan-400";
|
||||
case "Mystery":
|
||||
return "bg-amber-500/20 text-amber-400";
|
||||
case "Horror":
|
||||
return "bg-red-500/20 text-red-400";
|
||||
case "Slice of Life":
|
||||
return "bg-green-500/20 text-green-400";
|
||||
case "Historical":
|
||||
return "bg-orange-500/20 text-orange-400";
|
||||
default:
|
||||
return "bg-surface-700 text-surface-400";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -78,18 +97,19 @@
|
|||
if (result.success && result.storyId) {
|
||||
await story.loadAllStories();
|
||||
await story.loadStory(result.storyId);
|
||||
ui.setActivePanel('story');
|
||||
ui.setActivePanel("story");
|
||||
} else if (result.error) {
|
||||
importError = result.error;
|
||||
setTimeout(() => importError = null, 5000);
|
||||
setTimeout(() => (importError = null), 5000);
|
||||
}
|
||||
} catch (error) {
|
||||
importError = error instanceof Error ? error.message : 'Failed to read file';
|
||||
setTimeout(() => importError = null, 5000);
|
||||
importError =
|
||||
error instanceof Error ? error.message : "Failed to read file";
|
||||
setTimeout(() => (importError = null), 5000);
|
||||
}
|
||||
|
||||
// Reset file input for re-selection
|
||||
input.value = '';
|
||||
input.value = "";
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
@ -98,8 +118,12 @@
|
|||
<!-- Header -->
|
||||
<div class="mb-6 sm:mb-8 flex items-start justify-between gap-4 shrink-0">
|
||||
<div>
|
||||
<h1 class="text-xl sm:text-2xl font-bold text-surface-100">Story Library</h1>
|
||||
<p class="text-sm sm:text-base text-surface-400">Your adventures await</p>
|
||||
<h1 class="text-xl sm:text-2xl font-bold text-surface-100">
|
||||
Story Library
|
||||
</h1>
|
||||
<p class="text-sm sm:text-base text-surface-400">
|
||||
Your adventures await
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 flex-nowrap">
|
||||
<button
|
||||
|
|
@ -112,7 +136,7 @@
|
|||
</button>
|
||||
<button
|
||||
class="btn btn-secondary flex items-center gap-1.5 sm:gap-2 min-h-[44px] px-3 sm:px-4 text-sm"
|
||||
onclick={() => ui.setActivePanel('vault')}
|
||||
onclick={() => ui.setActivePanel("vault")}
|
||||
title="Vault - Manage reusable characters and lorebooks"
|
||||
>
|
||||
<Archive class="h-4 w-4 sm:h-5 sm:w-5" />
|
||||
|
|
@ -151,10 +175,16 @@
|
|||
|
||||
<!-- Stories grid -->
|
||||
{#if story.allStories.length === 0}
|
||||
<div class="flex flex-col items-center justify-center flex-1 text-center px-4 pb-20">
|
||||
<div
|
||||
class="flex flex-col items-center justify-center flex-1 text-center px-4 pb-20"
|
||||
>
|
||||
<BookOpen class="mb-2 h-12 w-12 sm:h-16 sm:w-16 text-surface-600" />
|
||||
<h2 class="text-lg sm:text-xl font-semibold text-surface-300">No stories yet</h2>
|
||||
<p class="mt-1 text-sm sm:text-base text-surface-500">Create your first adventure to get started</p>
|
||||
<h2 class="text-lg sm:text-xl font-semibold text-surface-300">
|
||||
No stories yet
|
||||
</h2>
|
||||
<p class="mt-1 text-sm sm:text-base text-surface-500">
|
||||
Create your first adventure to get started
|
||||
</p>
|
||||
<button
|
||||
class="btn btn-primary flex items-center justify-center gap-2 min-h-[48px] mt-4"
|
||||
onclick={openSetupWizard}
|
||||
|
|
@ -164,22 +194,30 @@
|
|||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="grid gap-3 sm:gap-4 grid-cols-1 xs:grid-cols-2 lg:grid-cols-3">
|
||||
<div
|
||||
class="grid gap-3 sm:gap-4 grid-cols-1 xs:grid-cols-2 lg:grid-cols-3"
|
||||
>
|
||||
{#each story.allStories as s (s.id)}
|
||||
<div
|
||||
role="button"
|
||||
tabindex="0"
|
||||
onclick={() => openStory(s.id)}
|
||||
onkeydown={(e) => e.key === 'Enter' && openStory(s.id)}
|
||||
onkeydown={(e) => e.key === "Enter" && openStory(s.id)}
|
||||
class="card group cursor-pointer text-left transition-colors hover:border-accent-500/50 hover:bg-surface-700/50 active:bg-surface-700 min-h-[80px]"
|
||||
>
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="font-semibold text-surface-100 group-hover:text-accent-400 truncate">
|
||||
<h3
|
||||
class="font-semibold text-surface-100 group-hover:text-accent-400 truncate"
|
||||
>
|
||||
{s.title}
|
||||
</h3>
|
||||
{#if s.genre}
|
||||
<span class="mt-1 inline-block rounded-full px-2 py-0.5 text-xs {getGenreColor(s.genre)}">
|
||||
<span
|
||||
class="mt-1 inline-block rounded-full px-2 py-0.5 text-xs {getGenreColor(
|
||||
s.genre,
|
||||
)}"
|
||||
>
|
||||
{s.genre}
|
||||
</span>
|
||||
{/if}
|
||||
|
|
@ -215,15 +253,17 @@
|
|||
class="hidden sm:flex fixed bottom-safe-4 left-safe-4 items-center gap-2 rounded-lg bg-[#5865F2] px-3 py-2 text-sm text-white shadow-lg transition-all hover:bg-[#4752C4] hover:scale-105"
|
||||
>
|
||||
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/>
|
||||
<path
|
||||
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="hidden sm:inline">Official Aventura Discord</span>
|
||||
<span class="hidden sm:inline">Official Aventuras Discord</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Setup Wizard -->
|
||||
{#if showSetupWizard}
|
||||
{#key setupWizardKey}
|
||||
<SetupWizard onClose={() => showSetupWizard = false} />
|
||||
<SetupWizard onClose={() => (showSetupWizard = false)} />
|
||||
{/key}
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
<script lang="ts">
|
||||
import { ui } from '$lib/stores/ui.svelte';
|
||||
import { story } from '$lib/stores/story.svelte';
|
||||
import { syncService } from '$lib/services/sync';
|
||||
import { exportService } from '$lib/services/export';
|
||||
import { getVersion } from '@tauri-apps/api/app';
|
||||
import { ui } from "$lib/stores/ui.svelte";
|
||||
import { story } from "$lib/stores/story.svelte";
|
||||
import { syncService } from "$lib/services/sync";
|
||||
import { exportService } from "$lib/services/export";
|
||||
import { getVersion } from "@tauri-apps/api/app";
|
||||
import {
|
||||
X,
|
||||
QrCode,
|
||||
|
|
@ -14,14 +14,14 @@
|
|||
AlertTriangle,
|
||||
RefreshCw,
|
||||
Check,
|
||||
} from 'lucide-svelte';
|
||||
import { Html5Qrcode } from 'html5-qrcode';
|
||||
} from "lucide-svelte";
|
||||
import { Html5Qrcode } from "html5-qrcode";
|
||||
import type {
|
||||
SyncServerInfo,
|
||||
SyncStoryPreview,
|
||||
SyncConnectionData,
|
||||
} from '$lib/types/sync';
|
||||
import { onDestroy } from 'svelte';
|
||||
} from "$lib/types/sync";
|
||||
import { onDestroy } from "svelte";
|
||||
|
||||
// State
|
||||
let serverInfo = $state<SyncServerInfo | null>(null);
|
||||
|
|
@ -51,7 +51,7 @@
|
|||
|
||||
// QR Scanner
|
||||
let scanner: Html5Qrcode | null = null;
|
||||
let scannerElementId = 'qr-reader';
|
||||
let scannerElementId = "qr-reader";
|
||||
|
||||
// Reset state when modal opens
|
||||
$effect(() => {
|
||||
|
|
@ -135,23 +135,28 @@
|
|||
|
||||
try {
|
||||
// If replacing, delete the existing story first
|
||||
const existingId = await syncService.findStoryIdByTitle(receivedStoryPreview.title);
|
||||
const existingId = await syncService.findStoryIdByTitle(
|
||||
receivedStoryPreview.title,
|
||||
);
|
||||
if (existingId) {
|
||||
await syncService.createPreSyncBackup(existingId);
|
||||
await syncService.deleteStory(existingId);
|
||||
}
|
||||
|
||||
const result = await exportService.importFromContent(receivedStoryJson, true);
|
||||
const result = await exportService.importFromContent(
|
||||
receivedStoryJson,
|
||||
true,
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
await story.loadAllStories();
|
||||
syncSuccess = true;
|
||||
syncMessage = `Successfully received "${receivedStoryPreview.title}"`;
|
||||
} else {
|
||||
error = result.error ?? 'Import failed';
|
||||
error = result.error ?? "Import failed";
|
||||
}
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Import failed';
|
||||
error = e instanceof Error ? e.message : "Import failed";
|
||||
} finally {
|
||||
loading = false;
|
||||
receivedStoryJson = null;
|
||||
|
|
@ -192,7 +197,7 @@
|
|||
}
|
||||
|
||||
async function startGenerateMode() {
|
||||
ui.setSyncMode('generate');
|
||||
ui.setSyncMode("generate");
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
|
|
@ -203,15 +208,15 @@
|
|||
// Start polling for pushed stories
|
||||
startPolling();
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to start server';
|
||||
ui.setSyncMode('select');
|
||||
error = e instanceof Error ? e.message : "Failed to start server";
|
||||
ui.setSyncMode("select");
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function startScanMode() {
|
||||
ui.setSyncMode('scan');
|
||||
ui.setSyncMode("scan");
|
||||
error = null;
|
||||
|
||||
// Wait for DOM to update
|
||||
|
|
@ -225,7 +230,7 @@
|
|||
|
||||
await scanner.start(
|
||||
{
|
||||
facingMode: 'environment',
|
||||
facingMode: "environment",
|
||||
},
|
||||
{
|
||||
fps: 10,
|
||||
|
|
@ -237,27 +242,36 @@
|
|||
},
|
||||
() => {
|
||||
// Ignore scan failures during continuous scanning
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Apply zoom after camera starts (more reliable on mobile)
|
||||
try {
|
||||
const videoElement = document.querySelector(`#${scannerElementId} video`) as HTMLVideoElement;
|
||||
const videoElement = document.querySelector(
|
||||
`#${scannerElementId} video`,
|
||||
) as HTMLVideoElement;
|
||||
if (videoElement && videoElement.srcObject) {
|
||||
const track = (videoElement.srcObject as MediaStream).getVideoTracks()[0];
|
||||
const capabilities = track.getCapabilities() as MediaTrackCapabilities & { zoom?: { min: number; max: number } };
|
||||
const track = (
|
||||
videoElement.srcObject as MediaStream
|
||||
).getVideoTracks()[0];
|
||||
const capabilities =
|
||||
track.getCapabilities() as MediaTrackCapabilities & {
|
||||
zoom?: { min: number; max: number };
|
||||
};
|
||||
if (capabilities.zoom) {
|
||||
const maxZoom = capabilities.zoom.max;
|
||||
const targetZoom = Math.min(maxZoom, 2.5);
|
||||
await track.applyConstraints({ advanced: [{ zoom: targetZoom } as MediaTrackConstraintSet] });
|
||||
await track.applyConstraints({
|
||||
advanced: [{ zoom: targetZoom } as MediaTrackConstraintSet],
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Zoom not supported on this device, continue without it
|
||||
}
|
||||
} catch (e) {
|
||||
error = 'Camera access denied or not available';
|
||||
ui.setSyncMode('select');
|
||||
error = "Camera access denied or not available";
|
||||
ui.setSyncMode("select");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -287,8 +301,8 @@
|
|||
// No mismatch, proceed normally
|
||||
await proceedWithConnection(parsed);
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Connection failed';
|
||||
ui.setSyncMode('select');
|
||||
error = e instanceof Error ? e.message : "Connection failed";
|
||||
ui.setSyncMode("select");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -297,7 +311,7 @@
|
|||
pendingConnection = null;
|
||||
remoteVersion = null;
|
||||
localVersion = null;
|
||||
ui.setSyncMode('select');
|
||||
ui.setSyncMode("select");
|
||||
}
|
||||
|
||||
async function proceedWithVersionMismatch() {
|
||||
|
|
@ -312,7 +326,7 @@
|
|||
async function proceedWithConnection(conn: SyncConnectionData) {
|
||||
try {
|
||||
connection = conn;
|
||||
ui.setSyncMode('connected');
|
||||
ui.setSyncMode("connected");
|
||||
|
||||
// Fetch available stories from remote
|
||||
loading = true;
|
||||
|
|
@ -328,8 +342,8 @@
|
|||
entryCount: 0, // We don't track this in the store
|
||||
}));
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Connection failed';
|
||||
ui.setSyncMode('select');
|
||||
error = e instanceof Error ? e.message : "Connection failed";
|
||||
ui.setSyncMode("select");
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
|
@ -339,21 +353,25 @@
|
|||
if (!connection || !selectedRemoteStory) return;
|
||||
|
||||
// Check for conflict
|
||||
const exists = await syncService.checkStoryExists(selectedRemoteStory.title);
|
||||
const exists = await syncService.checkStoryExists(
|
||||
selectedRemoteStory.title,
|
||||
);
|
||||
if (exists && !showConflictWarning) {
|
||||
conflictStoryTitle = selectedRemoteStory.title;
|
||||
showConflictWarning = true;
|
||||
return;
|
||||
}
|
||||
|
||||
ui.setSyncMode('syncing');
|
||||
ui.setSyncMode("syncing");
|
||||
loading = true;
|
||||
error = null;
|
||||
showConflictWarning = false;
|
||||
|
||||
try {
|
||||
// If replacing, delete the existing story first
|
||||
const existingId = await syncService.findStoryIdByTitle(selectedRemoteStory.title);
|
||||
const existingId = await syncService.findStoryIdByTitle(
|
||||
selectedRemoteStory.title,
|
||||
);
|
||||
if (existingId) {
|
||||
await syncService.createPreSyncBackup(existingId);
|
||||
await syncService.deleteStory(existingId);
|
||||
|
|
@ -362,7 +380,7 @@
|
|||
// Pull the story
|
||||
const storyJson = await syncService.pullStory(
|
||||
connection,
|
||||
selectedRemoteStory.id
|
||||
selectedRemoteStory.id,
|
||||
);
|
||||
|
||||
// Import using existing import service
|
||||
|
|
@ -374,10 +392,10 @@
|
|||
syncSuccess = true;
|
||||
syncMessage = `Successfully pulled "${selectedRemoteStory.title}"`;
|
||||
} else {
|
||||
error = result.error ?? 'Import failed';
|
||||
error = result.error ?? "Import failed";
|
||||
}
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Pull failed';
|
||||
error = e instanceof Error ? e.message : "Pull failed";
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
|
@ -386,7 +404,7 @@
|
|||
async function pushStory() {
|
||||
if (!connection || !selectedLocalStory) return;
|
||||
|
||||
ui.setSyncMode('syncing');
|
||||
ui.setSyncMode("syncing");
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
|
|
@ -395,7 +413,9 @@
|
|||
await syncService.createPreSyncBackup(selectedLocalStory.id);
|
||||
|
||||
// Export the story
|
||||
const storyJson = await syncService.exportStoryToJson(selectedLocalStory.id);
|
||||
const storyJson = await syncService.exportStoryToJson(
|
||||
selectedLocalStory.id,
|
||||
);
|
||||
|
||||
// Push to remote
|
||||
await syncService.pushStory(connection, storyJson);
|
||||
|
|
@ -403,7 +423,7 @@
|
|||
syncSuccess = true;
|
||||
syncMessage = `Successfully pushed "${selectedLocalStory.title}"`;
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Push failed';
|
||||
error = e instanceof Error ? e.message : "Push failed";
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
|
@ -420,10 +440,10 @@
|
|||
}
|
||||
|
||||
function formatDate(timestamp: number): string {
|
||||
return new Date(timestamp).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
return new Date(timestamp).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
|
@ -444,15 +464,15 @@
|
|||
<div class="flex items-center gap-2">
|
||||
<RefreshCw class="h-5 w-5 text-accent-400" />
|
||||
<h2 class="text-lg font-semibold text-surface-100">
|
||||
{#if ui.syncMode === 'select'}
|
||||
{#if ui.syncMode === "select"}
|
||||
Local Network Sync
|
||||
{:else if ui.syncMode === 'generate'}
|
||||
{:else if ui.syncMode === "generate"}
|
||||
Waiting for Connection
|
||||
{:else if ui.syncMode === 'scan'}
|
||||
{:else if ui.syncMode === "scan"}
|
||||
Scan QR Code
|
||||
{:else if ui.syncMode === 'connected'}
|
||||
{:else if ui.syncMode === "connected"}
|
||||
Select Story to Sync
|
||||
{:else if ui.syncMode === 'syncing'}
|
||||
{:else if ui.syncMode === "syncing"}
|
||||
Syncing...
|
||||
{/if}
|
||||
</h2>
|
||||
|
|
@ -490,7 +510,7 @@
|
|||
<p class="text-surface-400">{syncMessage}</p>
|
||||
<button class="btn btn-primary mt-6" onclick={close}>Done</button>
|
||||
</div>
|
||||
{:else if ui.syncMode === 'select'}
|
||||
{:else if ui.syncMode === "select"}
|
||||
<!-- Mode Selection -->
|
||||
<p class="text-surface-400 text-sm mb-4">
|
||||
Sync stories between devices on the same network.
|
||||
|
|
@ -527,7 +547,7 @@
|
|||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{:else if ui.syncMode === 'generate'}
|
||||
{:else if ui.syncMode === "generate"}
|
||||
<!-- QR Code Display -->
|
||||
{#if showReceivedConflict && receivedStoryPreview}
|
||||
<!-- Conflict warning for received push -->
|
||||
|
|
@ -541,11 +561,15 @@
|
|||
Story Already Exists
|
||||
</h3>
|
||||
<p class="text-surface-400 mb-4">
|
||||
A story named "{receivedStoryPreview.title}" already exists on this device.
|
||||
Replacing it will create a "Pre-sync backup" checkpoint first. Continue?
|
||||
A story named "{receivedStoryPreview.title}" already exists on
|
||||
this device. Replacing it will create a "Pre-sync backup"
|
||||
checkpoint first. Continue?
|
||||
</p>
|
||||
<div class="flex justify-center gap-3">
|
||||
<button class="btn btn-secondary" onclick={cancelReceivedImport}>
|
||||
<button
|
||||
class="btn btn-secondary"
|
||||
onclick={cancelReceivedImport}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button class="btn btn-primary" onclick={importReceivedStory}>
|
||||
|
|
@ -560,9 +584,7 @@
|
|||
</div>
|
||||
{:else if serverInfo}
|
||||
<div class="text-center">
|
||||
<div
|
||||
class="bg-white p-4 rounded-lg inline-block mx-auto mb-4"
|
||||
>
|
||||
<div class="bg-white p-4 rounded-lg inline-block mx-auto mb-4">
|
||||
<img
|
||||
src="data:image/png;base64,{serverInfo.qrCodeBase64}"
|
||||
alt="QR Code"
|
||||
|
|
@ -570,14 +592,14 @@
|
|||
/>
|
||||
</div>
|
||||
<p class="text-surface-400 text-sm">
|
||||
Scan this QR code with another device running Aventura
|
||||
Scan this QR code with another device running Aventuras
|
||||
</p>
|
||||
<p class="text-surface-500 text-xs mt-2">
|
||||
Server: {serverInfo.ip}:{serverInfo.port}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
{:else if ui.syncMode === 'scan'}
|
||||
{:else if ui.syncMode === "scan"}
|
||||
<!-- Version Mismatch Warning -->
|
||||
{#if showVersionWarning}
|
||||
<div class="text-center py-4">
|
||||
|
|
@ -590,20 +612,27 @@
|
|||
Version Mismatch
|
||||
</h3>
|
||||
<p class="text-surface-400 mb-2">
|
||||
The remote device is running a different version of Aventura.
|
||||
The remote device is running a different version of Aventuras.
|
||||
</p>
|
||||
<div class="text-sm text-surface-500 mb-4">
|
||||
<p>Local: v{localVersion}</p>
|
||||
<p>Remote: {remoteVersion ? `v${remoteVersion}` : 'unknown'}</p>
|
||||
<p>Remote: {remoteVersion ? `v${remoteVersion}` : "unknown"}</p>
|
||||
</div>
|
||||
<p class="text-surface-400 text-sm mb-4">
|
||||
Syncing between different versions may cause issues. Continue anyway?
|
||||
Syncing between different versions may cause issues. Continue
|
||||
anyway?
|
||||
</p>
|
||||
<div class="flex justify-center gap-3">
|
||||
<button class="btn btn-secondary" onclick={cancelVersionMismatch}>
|
||||
<button
|
||||
class="btn btn-secondary"
|
||||
onclick={cancelVersionMismatch}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button class="btn btn-primary" onclick={proceedWithVersionMismatch}>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
onclick={proceedWithVersionMismatch}
|
||||
>
|
||||
Continue Anyway
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -621,7 +650,7 @@
|
|||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
{:else if ui.syncMode === 'connected'}
|
||||
{:else if ui.syncMode === "connected"}
|
||||
<!-- Story Selection -->
|
||||
{#if loading}
|
||||
<div class="flex flex-col items-center justify-center py-8">
|
||||
|
|
@ -631,17 +660,15 @@
|
|||
{:else}
|
||||
<!-- Conflict Warning -->
|
||||
{#if showConflictWarning}
|
||||
<div
|
||||
class="mb-4 rounded-lg bg-amber-500/20 p-4 text-amber-400"
|
||||
>
|
||||
<div class="mb-4 rounded-lg bg-amber-500/20 p-4 text-amber-400">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<AlertTriangle class="h-5 w-5" />
|
||||
<span class="font-semibold">Story Already Exists</span>
|
||||
</div>
|
||||
<p class="text-sm">
|
||||
A story named "{conflictStoryTitle}" already exists on this
|
||||
device. Pulling will replace it after creating a "Pre-sync backup"
|
||||
checkpoint.
|
||||
device. Pulling will replace it after creating a "Pre-sync
|
||||
backup" checkpoint.
|
||||
</p>
|
||||
<div class="flex gap-2 mt-3">
|
||||
<button
|
||||
|
|
@ -692,7 +719,7 @@
|
|||
</div>
|
||||
<div class="text-xs text-surface-500 mt-1">
|
||||
{remoteStory.entryCount} entries • Updated {formatDate(
|
||||
remoteStory.updatedAt
|
||||
remoteStory.updatedAt,
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
|
|
@ -747,12 +774,12 @@
|
|||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{:else if ui.syncMode === 'syncing'}
|
||||
{:else if ui.syncMode === "syncing"}
|
||||
<!-- Syncing State -->
|
||||
<div class="flex flex-col items-center justify-center py-12">
|
||||
<Loader2 class="h-8 w-8 text-accent-400 animate-spin" />
|
||||
<p class="mt-4 text-surface-400">
|
||||
{selectedRemoteStory ? 'Pulling' : 'Pushing'} story...
|
||||
{selectedRemoteStory ? "Pulling" : "Pushing"} story...
|
||||
</p>
|
||||
<p class="text-surface-500 text-sm mt-1">Please wait</p>
|
||||
</div>
|
||||
|
|
@ -760,17 +787,25 @@
|
|||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
{#if ui.syncMode === 'connected' && !showConflictWarning && !loading && !syncSuccess}
|
||||
<div class="border-t border-surface-700 px-4 pt-4 pb-modal-safe -mx-4 -mb-4 sm:mx-0 sm:mb-0">
|
||||
{#if ui.syncMode === "connected" && !showConflictWarning && !loading && !syncSuccess}
|
||||
<div
|
||||
class="border-t border-surface-700 px-4 pt-4 pb-modal-safe -mx-4 -mb-4 sm:mx-0 sm:mb-0"
|
||||
>
|
||||
<div class="flex justify-end gap-2">
|
||||
<button class="btn btn-secondary" onclick={close}>Cancel</button>
|
||||
{#if selectedRemoteStory}
|
||||
<button class="btn btn-primary flex items-center gap-2" onclick={pullStory}>
|
||||
<button
|
||||
class="btn btn-primary flex items-center gap-2"
|
||||
onclick={pullStory}
|
||||
>
|
||||
<Download class="h-4 w-4" />
|
||||
Pull Story
|
||||
</button>
|
||||
{:else if selectedLocalStory}
|
||||
<button class="btn btn-primary flex items-center gap-2" onclick={pushStory}>
|
||||
<button
|
||||
class="btn btn-primary flex items-center gap-2"
|
||||
onclick={pushStory}
|
||||
>
|
||||
<Upload class="h-4 w-4" />
|
||||
Push Story
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { database } from '$lib/services/database';
|
||||
import { settings } from '$lib/stores/settings.svelte';
|
||||
import { grammarService } from '$lib/services/grammar';
|
||||
import { updaterService } from '$lib/services/updater';
|
||||
import AppShell from '$lib/components/layout/AppShell.svelte';
|
||||
import ProviderSetupModal from '$lib/components/settings/ProviderSetupModal.svelte';
|
||||
import { onMount } from "svelte";
|
||||
import { database } from "$lib/services/database";
|
||||
import { settings } from "$lib/stores/settings.svelte";
|
||||
import { grammarService } from "$lib/services/grammar";
|
||||
import { updaterService } from "$lib/services/updater";
|
||||
import AppShell from "$lib/components/layout/AppShell.svelte";
|
||||
import ProviderSetupModal from "$lib/components/settings/ProviderSetupModal.svelte";
|
||||
|
||||
let initialized = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
|
@ -31,22 +31,28 @@
|
|||
|
||||
// Check for updates on startup if enabled (don't await, run in background)
|
||||
if (settings.updateSettings.autoCheck) {
|
||||
const { checkInterval, lastChecked, autoDownload } = settings.updateSettings;
|
||||
const { checkInterval, lastChecked, autoDownload } =
|
||||
settings.updateSettings;
|
||||
const now = Date.now();
|
||||
const shouldCheck = checkInterval <= 0
|
||||
? true
|
||||
: !lastChecked || now - lastChecked >= checkInterval * 60 * 60 * 1000;
|
||||
const shouldCheck =
|
||||
checkInterval <= 0
|
||||
? true
|
||||
: !lastChecked ||
|
||||
now - lastChecked >= checkInterval * 60 * 60 * 1000;
|
||||
|
||||
if (shouldCheck) {
|
||||
updaterService.checkForUpdates()
|
||||
updaterService
|
||||
.checkForUpdates()
|
||||
.then(async (updateInfo) => {
|
||||
await settings.setLastChecked(Date.now());
|
||||
if (updateInfo.available) {
|
||||
console.log(`[Updater] Update available: v${updateInfo.version}`);
|
||||
console.log(
|
||||
`[Updater] Update available: v${updateInfo.version}`,
|
||||
);
|
||||
|
||||
// Auto-download if enabled
|
||||
if (autoDownload) {
|
||||
console.log('[Updater] Auto-downloading update...');
|
||||
console.log("[Updater] Auto-downloading update...");
|
||||
updaterService.downloadAndInstall().catch(console.error);
|
||||
}
|
||||
}
|
||||
|
|
@ -57,8 +63,9 @@
|
|||
|
||||
initialized = true;
|
||||
} catch (e) {
|
||||
console.error('Initialization error:', e);
|
||||
error = e instanceof Error ? e.message : 'Failed to initialize application';
|
||||
console.error("Initialization error:", e);
|
||||
error =
|
||||
e instanceof Error ? e.message : "Failed to initialize application";
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -77,7 +84,9 @@
|
|||
/>
|
||||
|
||||
{#if error}
|
||||
<div class="flex h-screen w-screen items-center justify-center bg-surface-900">
|
||||
<div
|
||||
class="flex h-screen w-screen items-center justify-center bg-surface-900"
|
||||
>
|
||||
<div class="card max-w-md text-center">
|
||||
<h1 class="text-xl font-semibold text-red-400">Initialization Error</h1>
|
||||
<p class="mt-2 text-surface-400">{error}</p>
|
||||
|
|
@ -91,16 +100,22 @@
|
|||
</div>
|
||||
{:else if showProviderSetup}
|
||||
<!-- Show loading background while provider setup is shown -->
|
||||
<div class="flex h-screen w-screen items-center justify-center bg-surface-900">
|
||||
<div
|
||||
class="flex h-screen w-screen items-center justify-center bg-surface-900"
|
||||
>
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<p class="text-surface-400">Welcome to Aventura</p>
|
||||
<p class="text-surface-400">Welcome to Aventuras</p>
|
||||
</div>
|
||||
</div>
|
||||
{:else if !initialized}
|
||||
<div class="flex h-screen w-screen items-center justify-center bg-surface-900">
|
||||
<div
|
||||
class="flex h-screen w-screen items-center justify-center bg-surface-900"
|
||||
>
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<div class="h-8 w-8 animate-spin rounded-full border-2 border-accent-500 border-t-transparent"></div>
|
||||
<p class="text-surface-400">Loading Aventura...</p>
|
||||
<div
|
||||
class="h-8 w-8 animate-spin rounded-full border-2 border-accent-500 border-t-transparent"
|
||||
></div>
|
||||
<p class="text-surface-400">Loading Aventuras...</p>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue