feat(auth): make auth works with desktop, web and embedded apps
This commit is contained in:
2
.github/workflows/test.yaml
vendored
2
.github/workflows/test.yaml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18'
|
||||
node-version: '22'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
|
||||
776
package-lock.json
generated
776
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
50
package.json
50
package.json
@@ -19,43 +19,43 @@
|
||||
"@agnosticeng/dv": "^0.0.14",
|
||||
"@agnosticeng/migrate": "^0.0.2",
|
||||
"@agnosticeng/sqlite": "^0.0.5",
|
||||
"@auth0/auth0-spa-js": "^2.1.3",
|
||||
"@floating-ui/dom": "^1.7.0",
|
||||
"@auth0/auth0-spa-js": "^2.7.0",
|
||||
"@floating-ui/dom": "^1.7.4",
|
||||
"@observablehq/plot": "^0.6.17",
|
||||
"@rich_harris/svelte-split-pane": "^2.0.0",
|
||||
"@tauri-apps/api": "^2.5.0",
|
||||
"@tauri-apps/plugin-deep-link": "^2.3.0",
|
||||
"@tauri-apps/plugin-dialog": "^2.2.2",
|
||||
"@tauri-apps/plugin-fs": "^2.3.0",
|
||||
"@tauri-apps/plugin-opener": "^2.2.7",
|
||||
"@tauri-apps/plugin-shell": "^2.2.1",
|
||||
"@tauri-apps/api": "^2.9.0",
|
||||
"@tauri-apps/plugin-deep-link": "^2.4.3",
|
||||
"@tauri-apps/plugin-dialog": "^2.4.0",
|
||||
"@tauri-apps/plugin-fs": "^2.4.2",
|
||||
"@tauri-apps/plugin-opener": "^2.5.0",
|
||||
"@tauri-apps/plugin-shell": "^2.3.1",
|
||||
"d3": "^7.9.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"dayjs": "^1.11.18",
|
||||
"highlight.js": "^11.11.1",
|
||||
"lodash": "^4.17.21",
|
||||
"marked": "^15.0.12",
|
||||
"marked-highlight": "^2.2.1",
|
||||
"marked": "^16.4.1",
|
||||
"marked-highlight": "^2.2.2",
|
||||
"mitt": "^3.0.1",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"monaco-editor": "^0.54.0",
|
||||
"normalize.css": "^8.0.1",
|
||||
"p-debounce": "^4.0.0",
|
||||
"sql-formatter": "^15.6.2"
|
||||
"p-debounce": "^5.0.0",
|
||||
"sql-formatter": "^15.6.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-static": "^3.0.8",
|
||||
"@sveltejs/kit": "^2.21.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||
"@tauri-apps/cli": "^2.5.0",
|
||||
"@sveltejs/adapter-static": "^3.0.10",
|
||||
"@sveltejs/kit": "^2.47.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||
"@tauri-apps/cli": "^2.9.1",
|
||||
"@types/d3": "^7.4.3",
|
||||
"@types/lodash": "^4.17.17",
|
||||
"@types/lodash": "^4.17.20",
|
||||
"@types/node": "^22.15.21",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier-plugin-svelte": "^3.4.0",
|
||||
"svelte": "^5.33.0",
|
||||
"svelte-check": "^4.2.1",
|
||||
"svelte": "^5.41.2",
|
||||
"svelte-check": "^4.3.3",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^6.3.5",
|
||||
"vite-plugin-devtools-json": "^0.1.0"
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.1.11",
|
||||
"vite-plugin-devtools-json": "^1.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
2062
src-tauri/Cargo.lock
generated
2062
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,86 +0,0 @@
|
||||
import { type Auth0Client, createAuth0Client } from '@auth0/auth0-spa-js';
|
||||
import { onOpenUrl } from '@tauri-apps/plugin-deep-link';
|
||||
import { openUrl as openUrlWithBrowser } from '@tauri-apps/plugin-opener';
|
||||
import mitt from 'mitt';
|
||||
|
||||
if (!AUTH0_DOMAIN) throw new Error('AUTH0_DOMAIN is not defined');
|
||||
if (!AUTH0_CLIENT_ID) throw new Error('AUTH0_CLIENT_ID is not defined');
|
||||
|
||||
const emitter = mitt<{ 'auth:changed': boolean }>();
|
||||
const noop = () => {};
|
||||
|
||||
let client: Auth0Client;
|
||||
async function init() {
|
||||
client = await createAuth0Client({
|
||||
domain: AUTH0_DOMAIN,
|
||||
clientId: AUTH0_CLIENT_ID,
|
||||
authorizationParams: {
|
||||
redirect_uri: AUTH0_REDIRECT_URI || window.location.origin
|
||||
},
|
||||
cacheLocation: 'localstorage',
|
||||
useRefreshTokens: true
|
||||
});
|
||||
|
||||
emitter.emit('auth:changed', await client.isAuthenticated());
|
||||
}
|
||||
|
||||
export async function checkLoginState() {
|
||||
await init();
|
||||
|
||||
if (PLATFORM === 'WEB') {
|
||||
if (window.location.search.includes('code=') && window.location.search.includes('state=')) {
|
||||
await client.handleRedirectCallback();
|
||||
window.history.replaceState({}, document.title, '/');
|
||||
emitter.emit('auth:changed', true);
|
||||
}
|
||||
}
|
||||
|
||||
if (PLATFORM === 'NATIVE') {
|
||||
await onOpenUrl(async (urls) => {
|
||||
const url = urls
|
||||
.map((u) => new URL(u))
|
||||
.find((u) => u.searchParams.has('code') && u.searchParams.has('state'));
|
||||
|
||||
if (url) {
|
||||
await client.handleRedirectCallback(url.toString());
|
||||
emitter.emit('auth:changed', true);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function onStateChange(cb: (authenticated: boolean) => unknown) {
|
||||
emitter.on('auth:changed', cb);
|
||||
return () => emitter.off('auth:changed', cb);
|
||||
}
|
||||
|
||||
export async function isAuthenticated() {
|
||||
if (client) return await client.isAuthenticated();
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function getToken() {
|
||||
if (client) {
|
||||
const tokens = await client.getTokenSilently({ detailedResponse: true, cacheMode: 'off' });
|
||||
return tokens.id_token;
|
||||
}
|
||||
}
|
||||
|
||||
export async function login() {
|
||||
if (client) await client.loginWithRedirect({ openUrl });
|
||||
}
|
||||
|
||||
export async function logout(silently = true) {
|
||||
if (client) {
|
||||
await client.logout({
|
||||
logoutParams: { returnTo: AUTH0_REDIRECT_URI || window.location.origin },
|
||||
openUrl: silently ? noop : openUrl
|
||||
});
|
||||
emitter.emit('auth:changed', false);
|
||||
}
|
||||
}
|
||||
|
||||
async function openUrl(url: string) {
|
||||
if (PLATFORM === 'WEB') window.location.assign(url);
|
||||
if (PLATFORM === 'NATIVE') await openUrlWithBrowser(url);
|
||||
}
|
||||
74
src/lib/auth/auth0-service.ts
Normal file
74
src/lib/auth/auth0-service.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { openUrl } from '$lib/env/open';
|
||||
import { Auth0Client, createAuth0Client } from '@auth0/auth0-spa-js';
|
||||
import { Notifier, type AuthService, type AuthSession } from './service';
|
||||
|
||||
export class Auth0AuthService extends Notifier<AuthSession> implements AuthService {
|
||||
private client: Auth0Client | null = null;
|
||||
|
||||
private async getClient() {
|
||||
if (!this.client)
|
||||
this.client = await createAuth0Client({
|
||||
domain: AUTH0_DOMAIN,
|
||||
clientId: AUTH0_CLIENT_ID,
|
||||
authorizationParams: {
|
||||
redirect_uri: AUTH0_REDIRECT_URI || window.location.origin
|
||||
},
|
||||
cacheLocation: 'localstorage',
|
||||
useRefreshTokens: true
|
||||
});
|
||||
|
||||
return this.client;
|
||||
}
|
||||
|
||||
async getSession() {
|
||||
const client = await this.getClient();
|
||||
try {
|
||||
const { id_token: token, expires_in } = await client.getTokenSilently({
|
||||
detailedResponse: true,
|
||||
cacheMode: 'off'
|
||||
});
|
||||
|
||||
const user = await client.getUser();
|
||||
const expiresAt = Date.now() + expires_in * 1000;
|
||||
|
||||
const session: AuthSession = { idToken: token, user, expiresAt };
|
||||
await this.setSession(session);
|
||||
return session;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async setSession(session: AuthSession) {
|
||||
this.notify(session);
|
||||
}
|
||||
|
||||
async clearSession() {
|
||||
const client = await this.getClient();
|
||||
await client.logout({
|
||||
logoutParams: { returnTo: AUTH0_REDIRECT_URI || window.location.origin },
|
||||
openUrl: () => {}
|
||||
});
|
||||
|
||||
this.notify(null);
|
||||
}
|
||||
|
||||
onStateChange(callback: (session: AuthSession | null) => void) {
|
||||
return this.register(callback);
|
||||
}
|
||||
|
||||
async login() {
|
||||
const client = await this.getClient();
|
||||
await client.loginWithRedirect({ openUrl });
|
||||
}
|
||||
|
||||
async handleRedirectCallback(url?: string) {
|
||||
const client = await this.getClient();
|
||||
const _result = await client.handleRedirectCallback(url);
|
||||
const user = await client.getUser();
|
||||
const tokens = await client.getTokenSilently({ detailedResponse: true, cacheMode: 'off' });
|
||||
const expiresAt = Date.now() + tokens.expires_in * 1000;
|
||||
const session: AuthSession = { idToken: tokens.id_token, user, expiresAt };
|
||||
await this.setSession(session);
|
||||
}
|
||||
}
|
||||
16
src/lib/auth/desktop-handler.ts
Normal file
16
src/lib/auth/desktop-handler.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { onOpenUrl } from '@tauri-apps/plugin-deep-link';
|
||||
import type { AuthService } from './service';
|
||||
|
||||
export class DesktopAuthHandler {
|
||||
constructor(private service: AuthService) {
|
||||
onOpenUrl(this.handleOpenUrls.bind(this));
|
||||
}
|
||||
|
||||
private async handleOpenUrls(urls: string[]) {
|
||||
const url = urls
|
||||
.map((u) => new URL(u))
|
||||
.find((u) => u.searchParams.has('code') && u.searchParams.has('state'));
|
||||
|
||||
if (url) await this.service.handleRedirectCallback?.(url.toString());
|
||||
}
|
||||
}
|
||||
18
src/lib/auth/embedded-handler.ts
Normal file
18
src/lib/auth/embedded-handler.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { AuthService, AuthSession } from './service';
|
||||
|
||||
export class EmbeddedAuthHandler {
|
||||
constructor(private service: AuthService) {
|
||||
this.handleURL();
|
||||
}
|
||||
|
||||
private async handleURL() {
|
||||
const url = new URL(window.location.href);
|
||||
const token = url.searchParams.get('token');
|
||||
if (token) {
|
||||
const decoded = JSON.parse(atob(token.split('.')[1]));
|
||||
const expiresAt = +(decoded.exp ?? Date.now() + 3600 * 1000);
|
||||
const session: AuthSession = { idToken: token, expiresAt };
|
||||
await this.service.setSession(session);
|
||||
}
|
||||
}
|
||||
}
|
||||
28
src/lib/auth/index.ts
Normal file
28
src/lib/auth/index.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { detectRuntime, type Runtime } from '$lib/env/runtime';
|
||||
import { Auth0AuthService } from './auth0-service';
|
||||
import { DesktopAuthHandler } from './desktop-handler';
|
||||
import { EmbeddedAuthHandler } from './embedded-handler';
|
||||
import { InMemoryAuthService } from './inmemory-service';
|
||||
import type { AuthService, AuthSession } from './service';
|
||||
import { WebAuthHandler } from './web-handler';
|
||||
|
||||
export function createAuthService(runtime: Runtime): AuthService {
|
||||
if (runtime === 'embedded') {
|
||||
return new InMemoryAuthService();
|
||||
}
|
||||
|
||||
return new Auth0AuthService();
|
||||
}
|
||||
|
||||
export function checkLoginState(runtime: Runtime, service: AuthService) {
|
||||
if (runtime === 'embedded') new EmbeddedAuthHandler(service);
|
||||
if (runtime === 'web') new WebAuthHandler(service);
|
||||
if (runtime === 'desktop') new DesktopAuthHandler(service);
|
||||
}
|
||||
|
||||
export function isAuthEnabled() {
|
||||
const runtime = detectRuntime();
|
||||
return runtime === 'embedded' || Boolean(AUTH0_CLIENT_ID && AUTH0_DOMAIN);
|
||||
}
|
||||
|
||||
export type { AuthService, AuthSession };
|
||||
23
src/lib/auth/inmemory-service.ts
Normal file
23
src/lib/auth/inmemory-service.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Notifier, type AuthService, type AuthSession } from './service';
|
||||
|
||||
export class InMemoryAuthService extends Notifier<AuthSession> implements AuthService {
|
||||
#session: AuthSession | null = null;
|
||||
|
||||
getSession() {
|
||||
return this.#session;
|
||||
}
|
||||
|
||||
setSession(session: AuthSession) {
|
||||
this.#session = session;
|
||||
this.notify(session);
|
||||
}
|
||||
|
||||
clearSession(): MaybePromise<void> {
|
||||
this.#session = null;
|
||||
this.notify(null);
|
||||
}
|
||||
|
||||
onStateChange(callback: (session: AuthSession | null) => void): () => void {
|
||||
return this.register(callback);
|
||||
}
|
||||
}
|
||||
27
src/lib/auth/service.ts
Normal file
27
src/lib/auth/service.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
export interface AuthSession {
|
||||
idToken: string;
|
||||
expiresAt: number;
|
||||
user?: { sub?: string; name?: string; email?: string };
|
||||
}
|
||||
|
||||
export interface AuthService {
|
||||
getSession(): MaybePromise<AuthSession | null>;
|
||||
setSession(session: AuthSession): MaybePromise<void>;
|
||||
clearSession(): MaybePromise<void>;
|
||||
onStateChange(callback: (session: AuthSession | null) => void): () => void;
|
||||
login?(): MaybePromise<void>;
|
||||
handleRedirectCallback?(url?: string): MaybePromise<void>;
|
||||
}
|
||||
|
||||
export abstract class Notifier<T> {
|
||||
private callbacks = new Set<(value: T | null) => void>();
|
||||
|
||||
protected register(callback: (value: T | null) => void) {
|
||||
this.callbacks.add(callback);
|
||||
return () => this.callbacks.delete(callback);
|
||||
}
|
||||
|
||||
protected notify(value: T | null) {
|
||||
this.callbacks.forEach((callback) => callback(value));
|
||||
}
|
||||
}
|
||||
18
src/lib/auth/web-handler.ts
Normal file
18
src/lib/auth/web-handler.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { AuthService } from './service';
|
||||
|
||||
export class WebAuthHandler {
|
||||
constructor(private service: AuthService) {
|
||||
this.handle();
|
||||
}
|
||||
|
||||
private async handle() {
|
||||
const url = new URL(window.location.href);
|
||||
if (url.searchParams.has('code') && url.searchParams.has('state')) {
|
||||
await this.service.handleRedirectCallback?.();
|
||||
|
||||
url.searchParams.delete('code');
|
||||
url.searchParams.delete('state');
|
||||
window.history.replaceState({}, document.title, url);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
interface Props {
|
||||
models: Model[];
|
||||
model: Model;
|
||||
model?: Model;
|
||||
onSelect?: (model: Model) => void;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<ul role="listbox">
|
||||
{#each models as m (m.name + m.brand)}
|
||||
{@const isSelected =
|
||||
m.brand === model.brand && m.name === model.name && m.baseURL === model.baseURL}
|
||||
m.brand === model?.brand && m.name === model?.name && m.baseURL === model?.baseURL}
|
||||
<li role="option" aria-selected={isSelected}>
|
||||
<button
|
||||
title={[m.brand, m.name].filter(Boolean).join(' • ')}
|
||||
@@ -27,6 +27,10 @@
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
{:else}
|
||||
<li role="option" aria-selected="false">
|
||||
<span>No model available</span>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
@@ -60,6 +64,12 @@
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
& > span {
|
||||
display: block;
|
||||
padding: 4px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
& > button {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { autoresize } from '$lib/actions/autoresize.svelte';
|
||||
import { scroll_to_bottom } from '$lib/actions/scrollToBottom.svelte';
|
||||
import { getToken, logout } from '$lib/auth';
|
||||
import Select from '$lib/components/Select.svelte';
|
||||
import { getAppContext } from '$lib/context';
|
||||
import ChevronDown from '$lib/icons/ChevronDown.svelte';
|
||||
@@ -26,7 +25,7 @@
|
||||
dataset?: Table;
|
||||
onOpenInEditor?: (sql: string) => void;
|
||||
models: Model[];
|
||||
model: Model;
|
||||
model?: Model;
|
||||
onModelChange: (m: Model) => void;
|
||||
}
|
||||
|
||||
@@ -50,8 +49,8 @@
|
||||
let modelSelectbox = $state<ReturnType<typeof Select>>();
|
||||
let form = $state<HTMLFormElement>();
|
||||
const uid = $props.id();
|
||||
const { isAuthenticated } = getAppContext();
|
||||
const needToLogin = $derived(isAgnosticModel(model) && !isAuthenticated());
|
||||
const { isAuthenticated, logout, getToken } = getAppContext();
|
||||
const needToLogin = $derived(model && isAgnosticModel(model) && !isAuthenticated());
|
||||
|
||||
function getContextFromTable(table: Table): string {
|
||||
const columns = table.columns.map((col) => `- ${col.name} (${col.type})`).join('\n');
|
||||
@@ -63,12 +62,13 @@
|
||||
});
|
||||
|
||||
onMount(() => form?.dispatchEvent(new SubmitEvent('submit')));
|
||||
const client = $derived(new OpenAIClient(model.baseURL));
|
||||
const client = $derived(model ? new OpenAIClient(model.baseURL) : null);
|
||||
|
||||
async function handleSubmit(
|
||||
event: SubmitEvent & { currentTarget: EventTarget & HTMLFormElement }
|
||||
) {
|
||||
event.preventDefault();
|
||||
if (!model) return;
|
||||
const form = event.currentTarget;
|
||||
|
||||
let token: string | undefined;
|
||||
@@ -93,7 +93,7 @@
|
||||
|
||||
try {
|
||||
abortController = new AbortController();
|
||||
const completion = await client.createChatCompletion(
|
||||
const completion = await client?.createChatCompletion(
|
||||
{
|
||||
model: model.name,
|
||||
messages: dataset
|
||||
@@ -104,7 +104,7 @@
|
||||
{ signal: abortController.signal, token }
|
||||
);
|
||||
|
||||
messages = messages.concat(completion.choices[0].message);
|
||||
if (completion) messages = messages.concat(completion.choices[0].message);
|
||||
} catch (e) {
|
||||
if (e === 'Canceled by user') {
|
||||
const last = messages.at(-1);
|
||||
@@ -194,7 +194,7 @@
|
||||
name="message"
|
||||
tabindex="0"
|
||||
rows="1"
|
||||
placeholder="Ask {model.name}"
|
||||
placeholder="Ask {model?.name ?? 'Ai'}"
|
||||
disabled={loading}
|
||||
use:autoresize
|
||||
bind:value={message}
|
||||
@@ -240,7 +240,7 @@
|
||||
disabled={models.length === 1}
|
||||
class="select-trigger"
|
||||
>
|
||||
<span>{model.name}</span>
|
||||
<span>{model?.name ?? 'No model selected'}</span>
|
||||
{#if models.length > 1}
|
||||
<ChevronDown size="12" />
|
||||
{/if}
|
||||
@@ -271,7 +271,7 @@
|
||||
type="submit"
|
||||
bind:this={submitter}
|
||||
title="Send ⌘⏎"
|
||||
disabled={needToLogin}
|
||||
disabled={!model || needToLogin}
|
||||
>
|
||||
Send ⌘⏎
|
||||
</button>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
onCloseAllTab?: () => void;
|
||||
onOpenInEditor?: (sql: string) => void;
|
||||
models: Model[];
|
||||
selectedModel: Model;
|
||||
selectedModel?: Model;
|
||||
onModelChange: (m: Model) => void;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,14 +13,14 @@ export interface Chat {
|
||||
|
||||
export type { Model };
|
||||
|
||||
export const ArgnosticModel: Model = {
|
||||
export const AgnosticModel: Model = {
|
||||
brand: 'Agnostic',
|
||||
name: 'Agnostic AI (v0)',
|
||||
baseURL: 'https://ai.agx.app/'
|
||||
};
|
||||
|
||||
export function isAgnosticModel(m: Model) {
|
||||
return m.baseURL === ArgnosticModel.baseURL;
|
||||
return m.baseURL === AgnosticModel.baseURL;
|
||||
}
|
||||
|
||||
export function serializeModel(model: Model) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { login } from '$lib/auth';
|
||||
import { getAppContext } from '$lib/context';
|
||||
import Sparkles from '$lib/icons/Sparkles.svelte';
|
||||
|
||||
const { login } = getAppContext();
|
||||
</script>
|
||||
|
||||
<div class="login-wrapper">
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import { getContext, setContext } from 'svelte';
|
||||
import type { AppContext } from './types';
|
||||
import type { ContextMenuState } from './components/ContextMenu';
|
||||
|
||||
export type AppContext = {
|
||||
contextmenu: ContextMenuState;
|
||||
isAuthenticated: () => boolean;
|
||||
login(): Promise<void>;
|
||||
logout(): Promise<void>;
|
||||
getToken(): Promise<string | undefined>;
|
||||
};
|
||||
|
||||
const key = Symbol('@app/context');
|
||||
|
||||
|
||||
8
src/lib/env/open.ts
vendored
Normal file
8
src/lib/env/open.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
import { openUrl as openUrlWithBrowser } from '@tauri-apps/plugin-opener';
|
||||
import { detectRuntime } from './runtime';
|
||||
|
||||
export async function openUrl(url: string) {
|
||||
const runtime = detectRuntime();
|
||||
if (runtime === 'desktop') await openUrlWithBrowser(url);
|
||||
else window.location.assign(url);
|
||||
}
|
||||
7
src/lib/env/runtime.ts
vendored
Normal file
7
src/lib/env/runtime.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
export function detectRuntime() {
|
||||
if (PLATFORM === 'NATIVE') return 'desktop';
|
||||
if (window.self !== window.top) return 'embedded';
|
||||
return 'web';
|
||||
}
|
||||
|
||||
export type Runtime = 'desktop' | 'embedded' | 'web';
|
||||
6
src/lib/types.d.ts
vendored
6
src/lib/types.d.ts
vendored
@@ -1,6 +0,0 @@
|
||||
import type { ContextMenuState } from './components/ContextMenu';
|
||||
|
||||
export type AppContext = {
|
||||
contextmenu: ContextMenuState;
|
||||
isAuthenticated(): boolean;
|
||||
};
|
||||
@@ -2,13 +2,16 @@
|
||||
import '$lib/styles/main.css';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
import { MIGRATIONS } from '$lib/migrations';
|
||||
import { store } from '$lib/store';
|
||||
import { MigrationManager } from '@agnosticeng/migrate';
|
||||
|
||||
import { checkLoginState, onStateChange } from '$lib/auth';
|
||||
import { createAuthService, checkLoginState, type AuthService } from '$lib/auth';
|
||||
import { detectRuntime, type Runtime } from '$lib/env/runtime';
|
||||
|
||||
import { ContextMenu, ContextMenuState } from '$lib/components/ContextMenu';
|
||||
import { setAppContext } from '$lib/context';
|
||||
import { MIGRATIONS } from '$lib/migrations';
|
||||
|
||||
import { EXAMPLES_TABS } from '$lib/onboarding';
|
||||
|
||||
let { children } = $props();
|
||||
@@ -16,7 +19,28 @@
|
||||
let authenticated = $state(false);
|
||||
|
||||
const contextmenu = new ContextMenuState();
|
||||
setAppContext({ contextmenu, isAuthenticated: () => authenticated });
|
||||
let authService = $state.raw<AuthService>();
|
||||
|
||||
setAppContext({
|
||||
contextmenu,
|
||||
|
||||
isAuthenticated() {
|
||||
return authenticated;
|
||||
},
|
||||
|
||||
async login() {
|
||||
await authService?.login?.();
|
||||
},
|
||||
|
||||
async logout() {
|
||||
await authService?.clearSession();
|
||||
},
|
||||
|
||||
async getToken() {
|
||||
const session = await authService?.getSession();
|
||||
return session?.idToken;
|
||||
}
|
||||
});
|
||||
|
||||
async function displayOnboarding() {
|
||||
for (const example of EXAMPLES_TABS) {
|
||||
@@ -27,9 +51,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => onStateChange((a) => (authenticated = a)));
|
||||
|
||||
onMount(async () => {
|
||||
const runtime = detectRuntime();
|
||||
|
||||
authService = createAuthService(runtime);
|
||||
authService.onStateChange((session) => (authenticated = !!session));
|
||||
|
||||
checkLoginState(runtime, authService);
|
||||
authenticated = !!(await authService.getSession());
|
||||
|
||||
const m = new MigrationManager(store);
|
||||
await m.migrate(MIGRATIONS);
|
||||
|
||||
@@ -39,8 +69,6 @@
|
||||
await displayOnboarding();
|
||||
}
|
||||
|
||||
await checkLoginState();
|
||||
|
||||
mounted = true;
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -266,7 +266,7 @@
|
||||
$effect(
|
||||
() =>
|
||||
void tabRepository.get().then(([t, active]) => {
|
||||
if (t.length) (tabs = t), (selectedTabIndex = active);
|
||||
if (t.length) ((tabs = t), (selectedTabIndex = active));
|
||||
else tabs.push({ id: crypto.randomUUID(), content: '', name: 'Untitled' });
|
||||
})
|
||||
);
|
||||
@@ -378,7 +378,7 @@ LIMIT 100;`;
|
||||
$effect(
|
||||
() =>
|
||||
void chatsRepository.list().then(([c, active]) => {
|
||||
if (c.length) (chats = c), (focusedChat = active);
|
||||
if (c.length) ((chats = c), (focusedChat = active));
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ArgnosticModel, type Model } from '$lib/components/Ai';
|
||||
import { isAuthEnabled } from '$lib/auth';
|
||||
import { AgnosticModel, type Model } from '$lib/components/Ai';
|
||||
import { engine } from '$lib/olap-engine';
|
||||
import { getModels, isInstalled } from '$lib/ollama';
|
||||
import type { PageLoad } from './$types';
|
||||
@@ -7,7 +8,9 @@ export const load = (async () => {
|
||||
await engine.init();
|
||||
|
||||
const isOllamaInstalled = await isInstalled();
|
||||
const models: Model[] = [ArgnosticModel];
|
||||
const models: Model[] = [];
|
||||
|
||||
if (isAuthEnabled()) models.push(AgnosticModel);
|
||||
if (isOllamaInstalled) models.push(...(await getModels()));
|
||||
|
||||
return { models };
|
||||
|
||||
@@ -31,10 +31,8 @@ export default defineConfig(async () => ({
|
||||
(process.env.CF_PAGES_COMMIT_SHA || process.env.COMMIT_SHA || 'dev').slice(0, 7)
|
||||
),
|
||||
OLLAMA_BASE_URL: JSON.stringify(process.env.OLLAMA_BASE_URL || 'http://localhost:11434'),
|
||||
AUTH0_DOMAIN: JSON.stringify(process.env.AUTH0_DOMAIN || 'agx.eu.auth0.com'),
|
||||
AUTH0_CLIENT_ID: JSON.stringify(
|
||||
process.env.AUTH0_CLIENT_ID || '12tfeh61h7wvyLXnJqf4X4YMKUc5j4Yq'
|
||||
),
|
||||
AUTH0_DOMAIN: JSON.stringify(process.env.AUTH0_DOMAIN || ''),
|
||||
AUTH0_CLIENT_ID: JSON.stringify(process.env.AUTH0_CLIENT_ID || ''),
|
||||
AUTH0_REDIRECT_URI: JSON.stringify(process.env.AUTH0_REDIRECT_URI || '')
|
||||
}
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user