mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-21 03:46:41 +02:00
chore: archive inactive projects to apps-archived/
Move inactive projects out of active workspace: - bauntown (community website) - maerchenzauber (AI story generation) - memoro (voice memo app) - news (news aggregation) - nutriphi (nutrition tracking) - reader (reading app) - uload (URL shortener) - wisekeep (AI wisdom extraction) Update CLAUDE.md documentation: - Add presi to active projects - Document archived projects section - Update workspace configuration Archived apps can be re-activated by moving back to apps/ 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
b97149ac12
commit
61d181fbc2
3148 changed files with 437 additions and 46640 deletions
13
apps-archived/wisekeep/apps/web/src/app.css
Normal file
13
apps-archived/wisekeep/apps/web/src/app.css
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--color-primary: theme('colors.primary.600');
|
||||
--color-primary-hover: theme('colors.primary.700');
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-gray-50 text-gray-900 antialiased;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
}
|
||||
13
apps-archived/wisekeep/apps/web/src/app.d.ts
vendored
Normal file
13
apps-archived/wisekeep/apps/web/src/app.d.ts
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// See https://kit.svelte.dev/docs/types#app
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
12
apps-archived/wisekeep/apps/web/src/app.html
Normal file
12
apps-archived/wisekeep/apps/web/src/app.html
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
107
apps-archived/wisekeep/apps/web/src/lib/api/client.ts
Normal file
107
apps-archived/wisekeep/apps/web/src/lib/api/client.ts
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import { env } from '$env/dynamic/public';
|
||||
|
||||
const API_BASE = env.PUBLIC_API_URL || 'http://localhost:3006';
|
||||
|
||||
export interface TranscriptionJob {
|
||||
id: string;
|
||||
url: string;
|
||||
language: string;
|
||||
provider: string;
|
||||
model?: string;
|
||||
status: 'pending' | 'downloading' | 'transcribing' | 'completed' | 'failed' | 'cancelled';
|
||||
progress: number;
|
||||
createdAt: string;
|
||||
completedAt?: string;
|
||||
videoInfo?: {
|
||||
id: string;
|
||||
title: string;
|
||||
channel: string;
|
||||
thumbnail: string;
|
||||
duration: number;
|
||||
};
|
||||
transcriptPath?: string;
|
||||
transcriptText?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface CreateJobRequest {
|
||||
url: string;
|
||||
language?: string;
|
||||
provider?: 'openai' | 'local';
|
||||
model?: 'tiny' | 'base' | 'small' | 'medium' | 'large';
|
||||
}
|
||||
|
||||
export interface Playlist {
|
||||
category: string;
|
||||
name: string;
|
||||
path: string;
|
||||
urlCount: number;
|
||||
urls: string[];
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface Stats {
|
||||
totalTranscripts: number;
|
||||
totalSizeMB: number;
|
||||
activeJobs: number;
|
||||
completedJobs: number;
|
||||
failedJobs: number;
|
||||
}
|
||||
|
||||
async function request<T>(endpoint: string, options?: RequestInit): Promise<T> {
|
||||
const res = await fetch(`${API_BASE}${endpoint}`, {
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options?.headers,
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const error = await res.json().catch(() => ({ message: res.statusText }));
|
||||
throw new Error(error.message || 'Request failed');
|
||||
}
|
||||
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export const api = {
|
||||
// Transcription
|
||||
createJob: (data: CreateJobRequest) =>
|
||||
request<TranscriptionJob>('/transcription', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
|
||||
getJob: (id: string) => request<TranscriptionJob>(`/transcription/${id}`),
|
||||
|
||||
getAllJobs: () => request<TranscriptionJob[]>('/transcription'),
|
||||
|
||||
cancelJob: (id: string) =>
|
||||
request<TranscriptionJob>(`/transcription/${id}`, { method: 'DELETE' }),
|
||||
|
||||
getStats: () => request<Stats>('/transcription/stats'),
|
||||
|
||||
// Playlists
|
||||
getPlaylists: () => request<Playlist[]>('/playlist'),
|
||||
|
||||
getPlaylist: (category: string, name: string) =>
|
||||
request<Playlist>(`/playlist/${category}/${name}`),
|
||||
|
||||
createPlaylist: (data: { name: string; description?: string; urls: string[] }) =>
|
||||
request<Playlist>('/playlist', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
|
||||
// Whisper
|
||||
getModels: () =>
|
||||
request<{
|
||||
models: { name: string; size: string; speed: string; accuracy: string }[];
|
||||
defaultProvider: string;
|
||||
openaiAvailable: boolean;
|
||||
}>('/whisper/models'),
|
||||
|
||||
// Health
|
||||
health: () => request<{ status: string }>('/health'),
|
||||
};
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<script lang="ts">
|
||||
import { AppSlider, type AppItem } from '@manacore/shared-ui';
|
||||
import { MANA_APPS, APP_STATUS_LABELS, APP_SLIDER_LABELS } from '@manacore/shared-branding';
|
||||
|
||||
// Convert MANA_APPS to AppItem format (German)
|
||||
const apps: AppItem[] = MANA_APPS.map((app) => ({
|
||||
name: app.name,
|
||||
description: app.description.de,
|
||||
longDescription: app.longDescription.de,
|
||||
icon: app.icon,
|
||||
color: app.color,
|
||||
comingSoon: app.comingSoon,
|
||||
status: app.status,
|
||||
}));
|
||||
|
||||
const statusLabels = APP_STATUS_LABELS.de;
|
||||
const labels = APP_SLIDER_LABELS.de;
|
||||
|
||||
function handleAppClick(app: AppItem, index: number) {
|
||||
console.log('Opening app:', app.name);
|
||||
}
|
||||
</script>
|
||||
|
||||
<AppSlider
|
||||
{apps}
|
||||
title={labels.title}
|
||||
isDark={false}
|
||||
{statusLabels}
|
||||
comingSoonLabel={labels.comingSoon}
|
||||
openAppLabel={labels.openApp}
|
||||
onAppClick={handleAppClick}
|
||||
/>
|
||||
204
apps-archived/wisekeep/apps/web/src/lib/stores/auth.svelte.ts
Normal file
204
apps-archived/wisekeep/apps/web/src/lib/stores/auth.svelte.ts
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
/**
|
||||
* Auth Store - Manages authentication state using Svelte 5 runes
|
||||
* Using Mana Core Auth
|
||||
*/
|
||||
|
||||
import { browser } from '$app/environment';
|
||||
import { initializeWebAuth, type UserData } from '@manacore/shared-auth';
|
||||
|
||||
// Initialize Mana Core Auth only on the client side
|
||||
const MANA_AUTH_URL = 'http://localhost:3001';
|
||||
|
||||
// Lazy initialization to avoid SSR issues with localStorage
|
||||
let _authService: ReturnType<typeof initializeWebAuth>['authService'] | null = null;
|
||||
let _tokenManager: ReturnType<typeof initializeWebAuth>['tokenManager'] | null = null;
|
||||
|
||||
function getAuthService() {
|
||||
if (!browser) return null;
|
||||
if (!_authService) {
|
||||
const auth = initializeWebAuth({ baseUrl: MANA_AUTH_URL });
|
||||
_authService = auth.authService;
|
||||
_tokenManager = auth.tokenManager;
|
||||
}
|
||||
return _authService;
|
||||
}
|
||||
|
||||
// State
|
||||
let user = $state<UserData | null>(null);
|
||||
let loading = $state(true);
|
||||
let initialized = $state(false);
|
||||
|
||||
export const authStore = {
|
||||
// Getters
|
||||
get user() {
|
||||
return user;
|
||||
},
|
||||
get loading() {
|
||||
return loading;
|
||||
},
|
||||
get isAuthenticated() {
|
||||
return !!user;
|
||||
},
|
||||
get initialized() {
|
||||
return initialized;
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize auth state from stored tokens
|
||||
*/
|
||||
async initialize() {
|
||||
if (initialized) return;
|
||||
|
||||
const authService = getAuthService();
|
||||
if (!authService) {
|
||||
initialized = true;
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
try {
|
||||
const authenticated = await authService.isAuthenticated();
|
||||
if (authenticated) {
|
||||
const userData = await authService.getUserFromToken();
|
||||
user = userData;
|
||||
}
|
||||
initialized = true;
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize auth:', error);
|
||||
user = null;
|
||||
initialized = true;
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Sign in with email and password
|
||||
*/
|
||||
async signIn(email: string, password: string) {
|
||||
const authService = getAuthService();
|
||||
if (!authService) {
|
||||
return { success: false, error: 'Auth not available on server' };
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await authService.signIn(email, password);
|
||||
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.error || 'Login failed' };
|
||||
}
|
||||
|
||||
// Get user data from token
|
||||
const userData = await authService.getUserFromToken();
|
||||
user = userData;
|
||||
|
||||
return { success: true, error: null };
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
return { success: false, error: errorMessage };
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Sign up with email and password
|
||||
*/
|
||||
async signUp(email: string, password: string) {
|
||||
const authService = getAuthService();
|
||||
if (!authService) {
|
||||
return { success: false, error: 'Auth not available on server', needsVerification: false };
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await authService.signUp(email, password);
|
||||
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.error || 'Signup failed', needsVerification: false };
|
||||
}
|
||||
|
||||
// Mana Core Auth requires separate login after signup
|
||||
if (result.needsVerification) {
|
||||
return { success: true, error: null, needsVerification: true };
|
||||
}
|
||||
|
||||
// Auto sign in after successful signup
|
||||
const signInResult = await this.signIn(email, password);
|
||||
return { ...signInResult, needsVerification: false };
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
return { success: false, error: errorMessage, needsVerification: false };
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Sign out
|
||||
*/
|
||||
async signOut() {
|
||||
const authService = getAuthService();
|
||||
if (!authService) {
|
||||
user = null;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await authService.signOut();
|
||||
user = null;
|
||||
} catch (error) {
|
||||
console.error('Sign out error:', error);
|
||||
// Clear user even if sign out fails
|
||||
user = null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Send password reset email
|
||||
*/
|
||||
async resetPassword(email: string) {
|
||||
const authService = getAuthService();
|
||||
if (!authService) {
|
||||
return { success: false, error: 'Auth not available on server' };
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await authService.forgotPassword(email);
|
||||
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.error || 'Password reset failed' };
|
||||
}
|
||||
|
||||
return { success: true, error: null };
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
return { success: false, error: errorMessage };
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get user credit balance
|
||||
*/
|
||||
async getCredits() {
|
||||
const authService = getAuthService();
|
||||
if (!authService) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const credits = await authService.getUserCredits();
|
||||
return credits;
|
||||
} catch (error) {
|
||||
console.error('Failed to get credits:', error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get access token for API calls
|
||||
*/
|
||||
async getAccessToken() {
|
||||
const authService = getAuthService();
|
||||
if (!authService) {
|
||||
return null;
|
||||
}
|
||||
return await authService.getAppToken();
|
||||
},
|
||||
};
|
||||
107
apps-archived/wisekeep/apps/web/src/lib/stores/jobs.ts
Normal file
107
apps-archived/wisekeep/apps/web/src/lib/stores/jobs.ts
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import { writable, derived, type Writable } from 'svelte/store';
|
||||
import { browser } from '$app/environment';
|
||||
import type { TranscriptionJob } from '$lib/api/client';
|
||||
|
||||
const API_URL = 'http://localhost:3006';
|
||||
const WS_URL = API_URL.replace('http', 'ws');
|
||||
|
||||
export const jobs: Writable<Map<string, TranscriptionJob>> = writable(new Map());
|
||||
export const isConnected = writable(false);
|
||||
|
||||
export const jobList = derived(jobs, ($jobs) =>
|
||||
Array.from($jobs.values()).sort(
|
||||
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
||||
)
|
||||
);
|
||||
|
||||
export const activeJobs = derived(jobList, ($jobs) =>
|
||||
$jobs.filter(
|
||||
(j) => j.status === 'pending' || j.status === 'downloading' || j.status === 'transcribing'
|
||||
)
|
||||
);
|
||||
|
||||
let socket: WebSocket | null = null;
|
||||
let reconnectTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
export function initWebSocket() {
|
||||
if (!browser) return;
|
||||
|
||||
const connect = () => {
|
||||
socket = new WebSocket(`${WS_URL}/progress`);
|
||||
|
||||
socket.onopen = () => {
|
||||
console.log('[WebSocket] Connected');
|
||||
isConnected.set(true);
|
||||
};
|
||||
|
||||
socket.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
if (data.type === 'heartbeat') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
data.type === 'job_update' ||
|
||||
data.type === 'job_complete' ||
|
||||
data.type === 'job_error'
|
||||
) {
|
||||
jobs.update((map) => {
|
||||
const existing = map.get(data.jobId);
|
||||
if (existing) {
|
||||
map.set(data.jobId, {
|
||||
...existing,
|
||||
status: data.status || existing.status,
|
||||
progress: data.progress ?? existing.progress,
|
||||
error: data.error || existing.error,
|
||||
videoInfo: data.videoInfo || existing.videoInfo,
|
||||
transcriptPath: data.transcriptPath || existing.transcriptPath,
|
||||
});
|
||||
}
|
||||
return new Map(map);
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[WebSocket] Parse error:', e);
|
||||
}
|
||||
};
|
||||
|
||||
socket.onclose = () => {
|
||||
console.log('[WebSocket] Disconnected');
|
||||
isConnected.set(false);
|
||||
|
||||
// Reconnect after 3 seconds
|
||||
reconnectTimeout = setTimeout(connect, 3000);
|
||||
};
|
||||
|
||||
socket.onerror = (error) => {
|
||||
console.error('[WebSocket] Error:', error);
|
||||
};
|
||||
};
|
||||
|
||||
connect();
|
||||
}
|
||||
|
||||
export function addJob(job: TranscriptionJob) {
|
||||
jobs.update((map) => {
|
||||
map.set(job.id, job);
|
||||
return new Map(map);
|
||||
});
|
||||
}
|
||||
|
||||
export function removeJob(id: string) {
|
||||
jobs.update((map) => {
|
||||
map.delete(id);
|
||||
return new Map(map);
|
||||
});
|
||||
}
|
||||
|
||||
export function cleanup() {
|
||||
if (reconnectTimeout) {
|
||||
clearTimeout(reconnectTimeout);
|
||||
}
|
||||
if (socket) {
|
||||
socket.close();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<script lang="ts">
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
{@render children()}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { ForgotPasswordPage } from '@manacore/shared-auth-ui';
|
||||
import { ManaCoreLogo } from '@manacore/shared-branding';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import AppSlider from '$lib/components/AppSlider.svelte';
|
||||
|
||||
// German translations
|
||||
const translations = {
|
||||
title: 'Passwort vergessen',
|
||||
subtitle: 'Gib deine E-Mail ein, um einen Reset-Link zu erhalten',
|
||||
emailPlaceholder: 'E-Mail',
|
||||
resetButton: 'Reset-Link senden',
|
||||
sending: 'Wird gesendet...',
|
||||
success: 'E-Mail gesendet!',
|
||||
backToLogin: 'Zurück zur Anmeldung',
|
||||
emailRequired: 'E-Mail ist erforderlich',
|
||||
emailInvalid: 'Bitte gib eine gültige E-Mail-Adresse ein',
|
||||
resetFailed: 'Zurücksetzen fehlgeschlagen',
|
||||
resetSuccess: 'Bitte überprüfe deine E-Mails',
|
||||
};
|
||||
|
||||
async function handleResetPassword(email: string) {
|
||||
return authStore.resetPassword(email);
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Passwort vergessen | Wisekeep</title>
|
||||
</svelte:head>
|
||||
|
||||
<ForgotPasswordPage
|
||||
appName="Wisekeep"
|
||||
logo={ManaCoreLogo}
|
||||
primaryColor="#8b5cf6"
|
||||
onResetPassword={handleResetPassword}
|
||||
{goto}
|
||||
loginPath="/login"
|
||||
lightBackground="#f3e8ff"
|
||||
darkBackground="#1e1b4b"
|
||||
{translations}
|
||||
>
|
||||
{#snippet appSlider()}
|
||||
<AppSlider />
|
||||
{/snippet}
|
||||
</ForgotPasswordPage>
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { LoginPage } from '@manacore/shared-auth-ui';
|
||||
import { ManaCoreLogo } from '@manacore/shared-branding';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import AppSlider from '$lib/components/AppSlider.svelte';
|
||||
|
||||
// Get redirect URL from query params
|
||||
const redirectTo = $derived($page.url.searchParams.get('redirectTo') || '/dashboard');
|
||||
|
||||
// German translations
|
||||
const translations = {
|
||||
title: 'Anmelden',
|
||||
subtitle: 'Melde dich mit deinem Konto an',
|
||||
emailPlaceholder: 'E-Mail',
|
||||
passwordPlaceholder: 'Passwort',
|
||||
rememberMe: 'Angemeldet bleiben',
|
||||
forgotPassword: 'Passwort vergessen?',
|
||||
signInButton: 'Anmelden',
|
||||
signingIn: 'Wird angemeldet...',
|
||||
success: 'Erfolgreich!',
|
||||
orDivider: 'oder',
|
||||
noAccount: 'Noch kein Konto?',
|
||||
createAccount: 'Jetzt registrieren',
|
||||
skipToForm: 'Zum Login-Formular springen',
|
||||
showPassword: 'Passwort anzeigen',
|
||||
hidePassword: 'Passwort verbergen',
|
||||
emailRequired: 'E-Mail ist erforderlich',
|
||||
emailInvalid: 'Bitte gib eine gültige E-Mail-Adresse ein',
|
||||
passwordRequired: 'Passwort ist erforderlich',
|
||||
signInFailed: 'Anmeldung fehlgeschlagen',
|
||||
googleSignInFailed: 'Google-Anmeldung fehlgeschlagen',
|
||||
signInSuccess: 'Erfolgreich angemeldet. Weiterleitung...',
|
||||
googleSignInSuccess: 'Erfolgreich mit Google angemeldet. Weiterleitung...',
|
||||
};
|
||||
|
||||
async function handleSignIn(email: string, password: string) {
|
||||
return authStore.signIn(email, password);
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Anmelden | Wisekeep</title>
|
||||
</svelte:head>
|
||||
|
||||
<LoginPage
|
||||
appName="Wisekeep"
|
||||
logo={ManaCoreLogo}
|
||||
primaryColor="#8b5cf6"
|
||||
onSignIn={handleSignIn}
|
||||
{goto}
|
||||
enableGoogle={false}
|
||||
enableApple={false}
|
||||
successRedirect={redirectTo}
|
||||
registerPath="/register"
|
||||
forgotPasswordPath="/forgot-password"
|
||||
lightBackground="#f3e8ff"
|
||||
darkBackground="#1e1b4b"
|
||||
{translations}
|
||||
>
|
||||
{#snippet appSlider()}
|
||||
<AppSlider />
|
||||
{/snippet}
|
||||
</LoginPage>
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { RegisterPage } from '@manacore/shared-auth-ui';
|
||||
import { ManaCoreLogo } from '@manacore/shared-branding';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import AppSlider from '$lib/components/AppSlider.svelte';
|
||||
|
||||
// German translations
|
||||
const translations = {
|
||||
title: 'Registrieren',
|
||||
subtitle: 'Erstelle dein Konto',
|
||||
emailPlaceholder: 'E-Mail',
|
||||
passwordPlaceholder: 'Passwort',
|
||||
confirmPasswordPlaceholder: 'Passwort wiederholen',
|
||||
signUpButton: 'Registrieren',
|
||||
signingUp: 'Wird registriert...',
|
||||
success: 'Erfolgreich!',
|
||||
orDivider: 'oder',
|
||||
hasAccount: 'Bereits ein Konto?',
|
||||
signIn: 'Jetzt anmelden',
|
||||
skipToForm: 'Zum Registrierungsformular springen',
|
||||
showPassword: 'Passwort anzeigen',
|
||||
hidePassword: 'Passwort verbergen',
|
||||
emailRequired: 'E-Mail ist erforderlich',
|
||||
emailInvalid: 'Bitte gib eine gültige E-Mail-Adresse ein',
|
||||
passwordRequired: 'Passwort ist erforderlich',
|
||||
passwordMinLength: 'Passwort muss mindestens 8 Zeichen haben',
|
||||
passwordsNotMatch: 'Passwörter stimmen nicht überein',
|
||||
signUpFailed: 'Registrierung fehlgeschlagen',
|
||||
signUpSuccess: 'Erfolgreich registriert. Weiterleitung...',
|
||||
verificationRequired: 'Bitte überprüfe deine E-Mails zur Bestätigung',
|
||||
};
|
||||
|
||||
async function handleSignUp(email: string, password: string) {
|
||||
return authStore.signUp(email, password);
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Registrieren | Wisekeep</title>
|
||||
</svelte:head>
|
||||
|
||||
<RegisterPage
|
||||
appName="Wisekeep"
|
||||
logo={ManaCoreLogo}
|
||||
primaryColor="#8b5cf6"
|
||||
onSignUp={handleSignUp}
|
||||
{goto}
|
||||
enableGoogle={false}
|
||||
enableApple={false}
|
||||
successRedirect="/"
|
||||
loginPath="/login"
|
||||
lightBackground="#f3e8ff"
|
||||
darkBackground="#1e1b4b"
|
||||
{translations}
|
||||
>
|
||||
{#snippet appSlider()}
|
||||
<AppSlider />
|
||||
{/snippet}
|
||||
</RegisterPage>
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
/**
|
||||
* Protected routes layout server
|
||||
* Auth checking is done client-side via Mana Core Auth
|
||||
*/
|
||||
|
||||
import type { LayoutServerLoad } from './$types';
|
||||
|
||||
export const load: LayoutServerLoad = async ({ url }) => {
|
||||
// Return the current path for client-side redirect logic
|
||||
return {
|
||||
pathname: url.pathname,
|
||||
};
|
||||
};
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { initWebSocket, cleanup, isConnected } from '$lib/stores/jobs';
|
||||
import type { LayoutData } from './$types';
|
||||
|
||||
let { children, data }: { children: any; data: LayoutData } = $props();
|
||||
|
||||
let isChecking = $state(true);
|
||||
|
||||
// Check auth on mount and redirect if not authenticated
|
||||
onMount(async () => {
|
||||
let shouldRedirect = false;
|
||||
|
||||
try {
|
||||
await authStore.initialize();
|
||||
shouldRedirect = !authStore.isAuthenticated;
|
||||
|
||||
if (!shouldRedirect) {
|
||||
// Initialize WebSocket after auth check
|
||||
initWebSocket();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Protected layout init error:', error);
|
||||
shouldRedirect = true;
|
||||
}
|
||||
|
||||
// Always set isChecking to false
|
||||
isChecking = false;
|
||||
|
||||
if (shouldRedirect) {
|
||||
const redirectTo = encodeURIComponent(data.pathname || '/dashboard');
|
||||
goto(`/login?redirectTo=${redirectTo}`);
|
||||
}
|
||||
|
||||
// Return cleanup function
|
||||
return () => cleanup();
|
||||
});
|
||||
|
||||
async function handleSignOut() {
|
||||
await authStore.signOut();
|
||||
goto('/login');
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if isChecking}
|
||||
<!-- Loading state while checking auth -->
|
||||
<div class="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600"></div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="min-h-screen flex flex-col">
|
||||
<header class="bg-white shadow-sm border-b">
|
||||
<div class="max-w-7xl mx-auto px-4 py-4 flex items-center justify-between">
|
||||
<a href="/dashboard" class="text-xl font-bold text-purple-600">Wisekeep</a>
|
||||
<nav class="flex items-center gap-6">
|
||||
<a
|
||||
href="/dashboard"
|
||||
class="transition-colors {$page.url.pathname === '/dashboard'
|
||||
? 'text-purple-600 font-medium'
|
||||
: 'text-gray-600 hover:text-gray-900'}"
|
||||
>
|
||||
Dashboard
|
||||
</a>
|
||||
<a
|
||||
href="/transcribe"
|
||||
class="transition-colors {$page.url.pathname === '/transcribe'
|
||||
? 'text-purple-600 font-medium'
|
||||
: 'text-gray-600 hover:text-gray-900'}"
|
||||
>
|
||||
Transcribe
|
||||
</a>
|
||||
<a
|
||||
href="/transcripts"
|
||||
class="transition-colors {$page.url.pathname === '/transcripts'
|
||||
? 'text-purple-600 font-medium'
|
||||
: 'text-gray-600 hover:text-gray-900'}"
|
||||
>
|
||||
Transcripts
|
||||
</a>
|
||||
<a
|
||||
href="/playlists"
|
||||
class="transition-colors {$page.url.pathname === '/playlists'
|
||||
? 'text-purple-600 font-medium'
|
||||
: 'text-gray-600 hover:text-gray-900'}"
|
||||
>
|
||||
Playlists
|
||||
</a>
|
||||
</nav>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-2 h-2 rounded-full {$isConnected ? 'bg-green-500' : 'bg-red-500'}"></span>
|
||||
<span class="text-sm text-gray-500">
|
||||
{$isConnected ? 'Connected' : 'Disconnected'}
|
||||
</span>
|
||||
</div>
|
||||
{#if authStore.user}
|
||||
<span class="text-sm text-gray-600 hidden sm:block">
|
||||
{authStore.user.email}
|
||||
</span>
|
||||
{/if}
|
||||
<button
|
||||
onclick={handleSignOut}
|
||||
class="px-3 py-2 text-sm font-medium text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
|
||||
>
|
||||
Abmelden
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="flex-1">
|
||||
{@render children()}
|
||||
</main>
|
||||
|
||||
<footer class="bg-gray-100 border-t py-4">
|
||||
<div class="max-w-7xl mx-auto px-4 text-center text-sm text-gray-500">
|
||||
Wisekeep - AI-powered wisdom extraction from video
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { api, type Stats } from '$lib/api/client';
|
||||
import { activeJobs, jobList } from '$lib/stores/jobs';
|
||||
|
||||
let stats: Stats | null = $state(null);
|
||||
let loading = $state(true);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
stats = await api.getStats();
|
||||
const jobs = await api.getAllJobs();
|
||||
// Initialize jobs store with existing jobs
|
||||
jobs.forEach((job) => {
|
||||
jobList; // trigger reactivity
|
||||
});
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to load stats';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Dashboard | Wisekeep</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="max-w-7xl mx-auto px-4 py-8">
|
||||
<h1 class="text-3xl font-bold mb-8">Dashboard</h1>
|
||||
|
||||
{#if loading}
|
||||
<div class="text-gray-500">Loading...</div>
|
||||
{:else if error}
|
||||
<div class="bg-red-50 text-red-600 p-4 rounded-lg">{error}</div>
|
||||
{:else if stats}
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||||
<div class="bg-white p-6 rounded-lg shadow-sm border">
|
||||
<div class="text-sm text-gray-500 mb-1">Total Transcripts</div>
|
||||
<div class="text-3xl font-bold text-purple-600">{stats.totalTranscripts}</div>
|
||||
</div>
|
||||
<div class="bg-white p-6 rounded-lg shadow-sm border">
|
||||
<div class="text-sm text-gray-500 mb-1">Storage Used</div>
|
||||
<div class="text-3xl font-bold">{stats.totalSizeMB} MB</div>
|
||||
</div>
|
||||
<div class="bg-white p-6 rounded-lg shadow-sm border">
|
||||
<div class="text-sm text-gray-500 mb-1">Active Jobs</div>
|
||||
<div class="text-3xl font-bold text-yellow-600">{stats.activeJobs}</div>
|
||||
</div>
|
||||
<div class="bg-white p-6 rounded-lg shadow-sm border">
|
||||
<div class="text-sm text-gray-500 mb-1">Completed</div>
|
||||
<div class="text-3xl font-bold text-green-600">{stats.completedJobs}</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="bg-white rounded-lg shadow-sm border p-6 mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4">Quick Start</h2>
|
||||
<a
|
||||
href="/transcribe"
|
||||
class="inline-flex items-center px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition"
|
||||
>
|
||||
Start New Transcription
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{#if $activeJobs.length > 0}
|
||||
<div class="bg-white rounded-lg shadow-sm border p-6">
|
||||
<h2 class="text-xl font-semibold mb-4">Active Jobs</h2>
|
||||
<div class="space-y-4">
|
||||
{#each $activeJobs as job (job.id)}
|
||||
<div class="border rounded-lg p-4">
|
||||
<div class="flex justify-between items-start mb-2">
|
||||
<div>
|
||||
<div class="font-medium">{job.videoInfo?.title || job.url}</div>
|
||||
<div class="text-sm text-gray-500">{job.videoInfo?.channel || 'Loading...'}</div>
|
||||
</div>
|
||||
<span
|
||||
class="px-2 py-1 text-xs rounded-full
|
||||
{job.status === 'downloading' ? 'bg-blue-100 text-blue-700' : ''}
|
||||
{job.status === 'transcribing' ? 'bg-yellow-100 text-yellow-700' : ''}
|
||||
{job.status === 'pending' ? 'bg-gray-100 text-gray-700' : ''}"
|
||||
>
|
||||
{job.status}
|
||||
</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
class="bg-purple-600 h-2 rounded-full transition-all"
|
||||
style="width: {job.progress}%"
|
||||
></div>
|
||||
</div>
|
||||
<div class="text-sm text-gray-500 mt-1">{job.progress}%</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { api, type Playlist } from '$lib/api/client';
|
||||
|
||||
let playlists = $state<Playlist[]>([]);
|
||||
let loading = $state(true);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
playlists = await api.getPlaylists();
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to load playlists';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
});
|
||||
|
||||
const groupedPlaylists = $derived(() => {
|
||||
const grouped: Record<string, Playlist[]> = {};
|
||||
for (const playlist of playlists) {
|
||||
if (!grouped[playlist.category]) {
|
||||
grouped[playlist.category] = [];
|
||||
}
|
||||
grouped[playlist.category].push(playlist);
|
||||
}
|
||||
return grouped;
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Playlists | Wisekeep</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="max-w-7xl mx-auto px-4 py-8">
|
||||
<h1 class="text-3xl font-bold mb-8">Playlists</h1>
|
||||
|
||||
{#if loading}
|
||||
<div class="text-gray-500">Loading...</div>
|
||||
{:else if error}
|
||||
<div class="bg-red-50 text-red-600 p-4 rounded-lg">{error}</div>
|
||||
{:else if playlists.length === 0}
|
||||
<div class="bg-gray-50 rounded-lg p-8 text-center">
|
||||
<p class="text-gray-500">No playlists yet</p>
|
||||
</div>
|
||||
{:else}
|
||||
{#each Object.entries(groupedPlaylists()) as [category, categoryPlaylists]}
|
||||
<div class="mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4 capitalize">{category}</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{#each categoryPlaylists as playlist}
|
||||
<div class="bg-white rounded-lg shadow-sm border p-4">
|
||||
<h3 class="font-medium">{playlist.name}</h3>
|
||||
{#if playlist.description}
|
||||
<p class="text-sm text-gray-500 mt-1">{playlist.description}</p>
|
||||
{/if}
|
||||
<p class="text-xs text-gray-400 mt-2">{playlist.urlCount} URLs</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
<script lang="ts">
|
||||
import { api } from '$lib/api/client';
|
||||
import { addJob } from '$lib/stores/jobs';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
let url = $state('');
|
||||
let language = $state('de');
|
||||
let provider = $state<'openai' | 'local'>('openai');
|
||||
let model = $state<'tiny' | 'base' | 'small' | 'medium' | 'large'>('base');
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
const languages = [
|
||||
{ code: 'de', name: 'German' },
|
||||
{ code: 'en', name: 'English' },
|
||||
{ code: 'es', name: 'Spanish' },
|
||||
{ code: 'fr', name: 'French' },
|
||||
{ code: 'it', name: 'Italian' },
|
||||
{ code: 'pt', name: 'Portuguese' },
|
||||
{ code: 'ja', name: 'Japanese' },
|
||||
{ code: 'ko', name: 'Korean' },
|
||||
{ code: 'zh', name: 'Chinese' },
|
||||
];
|
||||
|
||||
const models = [
|
||||
{ value: 'tiny', label: 'Tiny (39 MB, ~10x speed)' },
|
||||
{ value: 'base', label: 'Base (74 MB, ~7x speed)' },
|
||||
{ value: 'small', label: 'Small (244 MB, ~4x speed)' },
|
||||
{ value: 'medium', label: 'Medium (769 MB, ~2x speed)' },
|
||||
{ value: 'large', label: 'Large (1.5 GB, best accuracy)' },
|
||||
];
|
||||
|
||||
async function handleSubmit(e: Event) {
|
||||
e.preventDefault();
|
||||
error = null;
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
const job = await api.createJob({
|
||||
url,
|
||||
language,
|
||||
provider,
|
||||
model: provider === 'local' ? model : undefined,
|
||||
});
|
||||
addJob(job);
|
||||
goto('/');
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to start transcription';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>New Transcription | Wisekeep</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="max-w-2xl mx-auto px-4 py-8">
|
||||
<h1 class="text-3xl font-bold mb-8">New Transcription</h1>
|
||||
|
||||
{#if error}
|
||||
<div class="bg-red-50 text-red-600 p-4 rounded-lg mb-6">{error}</div>
|
||||
{/if}
|
||||
|
||||
<form onsubmit={handleSubmit} class="bg-white rounded-lg shadow-sm border p-6 space-y-6">
|
||||
<div>
|
||||
<label for="url" class="block text-sm font-medium text-gray-700 mb-2"> YouTube URL </label>
|
||||
<input
|
||||
type="url"
|
||||
id="url"
|
||||
bind:value={url}
|
||||
placeholder="https://www.youtube.com/watch?v=..."
|
||||
required
|
||||
class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="language" class="block text-sm font-medium text-gray-700 mb-2"> Language </label>
|
||||
<select
|
||||
id="language"
|
||||
bind:value={language}
|
||||
class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
||||
>
|
||||
{#each languages as lang}
|
||||
<option value={lang.code}>{lang.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2"> Transcription Provider </label>
|
||||
<div class="flex gap-4">
|
||||
<label class="flex items-center gap-2">
|
||||
<input type="radio" bind:group={provider} value="openai" />
|
||||
<span>OpenAI Whisper API</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2">
|
||||
<input type="radio" bind:group={provider} value="local" />
|
||||
<span>Local Whisper</span>
|
||||
</label>
|
||||
</div>
|
||||
<p class="text-sm text-gray-500 mt-1">
|
||||
{provider === 'openai'
|
||||
? 'Fast, cloud-based transcription (~$0.006/min)'
|
||||
: 'Free, requires local Whisper installation'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{#if provider === 'local'}
|
||||
<div>
|
||||
<label for="model" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Whisper Model
|
||||
</label>
|
||||
<select
|
||||
id="model"
|
||||
bind:value={model}
|
||||
class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
||||
>
|
||||
{#each models as m}
|
||||
<option value={m.value}>{m.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading || !url}
|
||||
class="w-full py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loading ? 'Starting...' : 'Start Transcription'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { api } from '$lib/api/client';
|
||||
import { jobList } from '$lib/stores/jobs';
|
||||
|
||||
let loading = $state(true);
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
const jobs = await api.getAllJobs();
|
||||
// Jobs are managed via the store
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
});
|
||||
|
||||
const completedJobs = $derived($jobList.filter((j) => j.status === 'completed'));
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Transcripts | Wisekeep</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="max-w-7xl mx-auto px-4 py-8">
|
||||
<h1 class="text-3xl font-bold mb-8">Transcripts</h1>
|
||||
|
||||
{#if loading}
|
||||
<div class="text-gray-500">Loading...</div>
|
||||
{:else if completedJobs.length === 0}
|
||||
<div class="bg-gray-50 rounded-lg p-8 text-center">
|
||||
<p class="text-gray-500 mb-4">No transcripts yet</p>
|
||||
<a
|
||||
href="/transcribe"
|
||||
class="inline-flex items-center px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700"
|
||||
>
|
||||
Create your first transcript
|
||||
</a>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="grid gap-4">
|
||||
{#each completedJobs as job (job.id)}
|
||||
<div class="bg-white rounded-lg shadow-sm border p-4">
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 class="font-medium">{job.videoInfo?.title || 'Untitled'}</h3>
|
||||
<p class="text-sm text-gray-500">{job.videoInfo?.channel || 'Unknown channel'}</p>
|
||||
<p class="text-xs text-gray-400 mt-1">
|
||||
Completed: {new Date(job.completedAt || '').toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
<span class="px-2 py-1 text-xs bg-green-100 text-green-700 rounded-full">
|
||||
Completed
|
||||
</span>
|
||||
</div>
|
||||
{#if job.transcriptText}
|
||||
<details class="mt-4">
|
||||
<summary class="cursor-pointer text-sm text-purple-600 hover:text-purple-700">
|
||||
View transcript
|
||||
</summary>
|
||||
<pre
|
||||
class="mt-2 p-4 bg-gray-50 rounded text-sm whitespace-pre-wrap overflow-auto max-h-96">
|
||||
{job.transcriptText}
|
||||
</pre>
|
||||
</details>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<script lang="ts">
|
||||
import '../app.css';
|
||||
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<div class="min-h-screen bg-gray-50">
|
||||
{@render children()}
|
||||
</div>
|
||||
27
apps-archived/wisekeep/apps/web/src/routes/+page.svelte
Normal file
27
apps-archived/wisekeep/apps/web/src/routes/+page.svelte
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
|
||||
onMount(async () => {
|
||||
await authStore.initialize();
|
||||
if (authStore.isAuthenticated) {
|
||||
goto('/dashboard', { replaceState: true });
|
||||
} else {
|
||||
goto('/login', { replaceState: true });
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Wisekeep - AI Wisdom Extraction</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="flex items-center justify-center min-h-screen">
|
||||
<div class="text-center">
|
||||
<div
|
||||
class="animate-spin w-10 h-10 border-4 border-purple-500 border-r-transparent rounded-full mx-auto"
|
||||
></div>
|
||||
<p class="mt-4 text-gray-600">Wird geladen...</p>
|
||||
</div>
|
||||
</div>
|
||||
Loading…
Add table
Add a link
Reference in a new issue