mirror of
https://github.com/anotherhadi/iknowyou.git
synced 2026-04-11 16:37:25 +02:00
347 lines
11 KiB
Svelte
347 lines
11 KiB
Svelte
<script>
|
|
import { onMount } from "svelte";
|
|
import { AlertTriangle } from "@lucide/svelte";
|
|
import Select from "./comps/Select.svelte";
|
|
import { INPUT_TYPES } from "@src/lib/vars";
|
|
import ToolIcon from "./comps/ToolIcon.svelte";
|
|
|
|
let tools = $state([]);
|
|
let config = $state({ tools: {}, profiles: {} });
|
|
let profileSummaries = $state([]);
|
|
let selectedProfile = $state("default");
|
|
let profileDetail = $state(null);
|
|
let loading = $state(true);
|
|
let profileLoading = $state(false);
|
|
let error = $state("");
|
|
|
|
let selectedInputType = $state("all");
|
|
|
|
const inputTypeOptions = ["all", ...INPUT_TYPES];
|
|
|
|
let profileOptions = $derived(profileSummaries.map((p) => p.name));
|
|
|
|
let activeSet = $derived(
|
|
profileDetail
|
|
? new Set(profileDetail.active_tools ?? [])
|
|
: new Set(tools.map((t) => t.name)),
|
|
);
|
|
|
|
let globalToolConf = $derived(config.tools ?? {});
|
|
let profileOverrides = $derived(profileDetail?.tools ?? {});
|
|
|
|
let toolsWithStatus = $derived(
|
|
tools.map((tool) => {
|
|
const isActive = activeSet.has(tool.name);
|
|
const effective = {
|
|
...(globalToolConf[tool.name] ?? {}),
|
|
...(profileOverrides[tool.name] ?? {}),
|
|
};
|
|
const missingConfig = (tool.config_fields ?? []).some((f) => {
|
|
if (!f.required) return false;
|
|
const v = effective[f.name];
|
|
return v === undefined || v === null || v === "";
|
|
});
|
|
const unavailable = tool.available === false;
|
|
return { ...tool, isActive, missingConfig, unavailable };
|
|
}),
|
|
);
|
|
|
|
let visibleTools = $derived(
|
|
selectedInputType === "all"
|
|
? toolsWithStatus
|
|
: toolsWithStatus.filter((t) =>
|
|
t.input_types.includes(selectedInputType),
|
|
),
|
|
);
|
|
|
|
let active = $derived(
|
|
visibleTools.filter(
|
|
(t) => t.isActive && !t.missingConfig && !t.unavailable,
|
|
),
|
|
);
|
|
let activeMissing = $derived(
|
|
visibleTools.filter((t) => t.isActive && t.missingConfig && !t.unavailable),
|
|
);
|
|
let activeUnavail = $derived(
|
|
visibleTools.filter((t) => t.isActive && t.unavailable),
|
|
);
|
|
let inactive = $derived(
|
|
visibleTools.filter(
|
|
(t) => !t.isActive && !t.missingConfig && !t.unavailable,
|
|
),
|
|
);
|
|
let inactiveMissing = $derived(
|
|
visibleTools.filter(
|
|
(t) => !t.isActive && t.missingConfig && !t.unavailable,
|
|
),
|
|
);
|
|
let inactiveUnavail = $derived(
|
|
visibleTools.filter((t) => !t.isActive && t.unavailable),
|
|
);
|
|
|
|
onMount(async () => {
|
|
try {
|
|
const [tr, cr, pr] = await Promise.all([
|
|
fetch("/api/tools"),
|
|
fetch("/api/config"),
|
|
fetch("/api/config/profiles"),
|
|
]);
|
|
if (!tr.ok) throw new Error(`HTTP ${tr.status}`);
|
|
if (!cr.ok) throw new Error(`HTTP ${cr.status}`);
|
|
if (!pr.ok) throw new Error(`HTTP ${pr.status}`);
|
|
tools = await tr.json();
|
|
config = await cr.json();
|
|
profileSummaries = await pr.json();
|
|
} catch (e) {
|
|
error = e.message;
|
|
} finally {
|
|
loading = false;
|
|
}
|
|
});
|
|
|
|
async function selectProfile(name) {
|
|
selectedProfile = name;
|
|
profileLoading = true;
|
|
try {
|
|
const res = await fetch(
|
|
`/api/config/profiles/${encodeURIComponent(name)}`,
|
|
);
|
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
profileDetail = await res.json();
|
|
} catch (e) {
|
|
error = e.message;
|
|
} finally {
|
|
profileLoading = false;
|
|
}
|
|
}
|
|
</script>
|
|
|
|
{#snippet toolCard(tool, missing)}
|
|
<div
|
|
class="card bg-base-200 group-hover:bg-base-300 transition-colors shadow-sm h-full
|
|
{missing ? 'border border-warning/40' : ''}
|
|
{tool.unavailable ? 'border border-error/40' : ''}"
|
|
>
|
|
<div class="card-body p-4 flex-row items-start gap-0">
|
|
<div
|
|
class="size-10 rounded-lg bg-base-300 group-hover:bg-base-200 transition-colors
|
|
flex items-center justify-center shrink-0 mr-3 mt-0.5"
|
|
>
|
|
<ToolIcon iconName={tool.icon} size={20} />
|
|
</div>
|
|
|
|
<div class="flex flex-col min-w-0 flex-1 gap-1.5">
|
|
<div class="flex items-center gap-2 flex-wrap">
|
|
<span class="font-bold text-sm leading-tight">{tool.name}</span>
|
|
{#if tool.unavailable}
|
|
<span class="badge badge-error badge-xs gap-1">
|
|
<AlertTriangle size={9} /> unavailable
|
|
</span>
|
|
{:else if missing}
|
|
<span class="badge badge-warning badge-xs gap-1">
|
|
<AlertTriangle size={9} /> config required
|
|
</span>
|
|
{/if}
|
|
</div>
|
|
{#if tool.unavailable && tool.unavailable_reason}
|
|
<p class="text-xs text-error/70 leading-relaxed">
|
|
{tool.unavailable_reason}
|
|
</p>
|
|
{:else if tool.description}
|
|
<p class="text-xs text-base-content/50 line-clamp-2 leading-relaxed">
|
|
{tool.description}
|
|
</p>
|
|
{/if}
|
|
<div class="flex flex-wrap gap-1">
|
|
{#each tool.input_types as t}
|
|
<span class="badge badge-xs badge-outline border-base-content/20">{t}</span>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/snippet}
|
|
|
|
{#if loading}
|
|
<div class="flex justify-center py-12">
|
|
<span class="loading loading-spinner loading-md"></span>
|
|
</div>
|
|
{:else if error}
|
|
<div class="alert alert-error gap-3"><AlertTriangle size={18} class="shrink-0" />{error}</div>
|
|
{:else}
|
|
<div class="flex flex-wrap items-center gap-x-6 gap-y-3 mb-6">
|
|
<div class="flex items-center gap-3">
|
|
<span
|
|
class="text-xs uppercase tracking-widest text-base-content/50 shrink-0"
|
|
>Profile</span
|
|
>
|
|
<Select
|
|
options={profileOptions}
|
|
selected={selectedProfile}
|
|
onselect={selectProfile}
|
|
/>
|
|
{#if profileLoading}
|
|
<span class="loading loading-spinner loading-xs opacity-40"></span>
|
|
{/if}
|
|
</div>
|
|
|
|
<div class="flex items-center gap-3">
|
|
<span
|
|
class="text-xs uppercase tracking-widest text-base-content/50 shrink-0"
|
|
>Input</span
|
|
>
|
|
<Select
|
|
options={inputTypeOptions}
|
|
selected={selectedInputType}
|
|
onselect={(val) => (selectedInputType = val)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{#if tools.length === 0}
|
|
<p class="text-base-content/40 text-sm text-center py-8">
|
|
No tools registered.
|
|
</p>
|
|
{:else}
|
|
<div class="flex flex-col gap-6">
|
|
{#if active.length > 0}
|
|
<section>
|
|
<div class="flex items-center gap-2 mb-3">
|
|
<span class="size-1.5 rounded-full bg-success shrink-0"></span>
|
|
<span class="text-xs uppercase tracking-widest text-base-content/50"
|
|
>Active</span
|
|
>
|
|
<span class="text-xs text-base-content/30">{active.length}</span>
|
|
</div>
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
|
{#each active as tool}
|
|
<a href="/tools/{tool.name}" class="group">
|
|
{@render toolCard(tool, false)}
|
|
</a>
|
|
{/each}
|
|
</div>
|
|
</section>
|
|
{/if}
|
|
|
|
{#if activeMissing.length > 0}
|
|
<section>
|
|
<div class="flex items-center gap-2 mb-3">
|
|
<span class="size-1.5 rounded-full bg-warning shrink-0"></span>
|
|
<span class="text-xs uppercase tracking-widest text-base-content/50"
|
|
>Active - required config missing</span
|
|
>
|
|
<span class="text-xs text-base-content/30"
|
|
>{activeMissing.length}</span
|
|
>
|
|
</div>
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
|
{#each activeMissing as tool}
|
|
<a href="/tools/{tool.name}" class="group">
|
|
{@render toolCard(tool, true)}
|
|
</a>
|
|
{/each}
|
|
</div>
|
|
</section>
|
|
{/if}
|
|
|
|
{#if activeUnavail.length > 0}
|
|
<section>
|
|
<div class="flex items-center gap-2 mb-3">
|
|
<span class="size-1.5 rounded-full bg-error shrink-0"></span>
|
|
<span class="text-xs uppercase tracking-widest text-error/70"
|
|
>Active - unavailable</span
|
|
>
|
|
<span class="text-xs text-base-content/30"
|
|
>{activeUnavail.length}</span
|
|
>
|
|
</div>
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
|
{#each activeUnavail as tool}
|
|
<a href="/tools/{tool.name}" class="group">
|
|
{@render toolCard(tool, false)}
|
|
</a>
|
|
{/each}
|
|
</div>
|
|
</section>
|
|
{/if}
|
|
|
|
{#if active.length + activeMissing.length + activeUnavail.length > 0 && inactive.length + inactiveMissing.length + inactiveUnavail.length > 0}
|
|
<div class="divider opacity-20 my-0"></div>
|
|
{/if}
|
|
|
|
{#if inactive.length > 0}
|
|
<section>
|
|
<div class="flex items-center gap-2 mb-3">
|
|
<span class="size-1.5 rounded-full bg-base-content/20 shrink-0"
|
|
></span>
|
|
<span class="text-xs uppercase tracking-widest text-base-content/30"
|
|
>Disabled</span
|
|
>
|
|
<span class="text-xs text-base-content/20">{inactive.length}</span>
|
|
</div>
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 opacity-40">
|
|
{#each inactive as tool}
|
|
<a
|
|
href="/tools/{tool.name}"
|
|
class="group hover:opacity-100 transition-opacity"
|
|
>
|
|
{@render toolCard(tool, false)}
|
|
</a>
|
|
{/each}
|
|
</div>
|
|
</section>
|
|
{/if}
|
|
|
|
{#if inactiveMissing.length > 0}
|
|
<section>
|
|
<div class="flex items-center gap-2 mb-3">
|
|
<span class="size-1.5 rounded-full bg-base-content/20 shrink-0"
|
|
></span>
|
|
<span class="text-xs uppercase tracking-widest text-base-content/30"
|
|
>Disabled - required config missing</span
|
|
>
|
|
<span class="text-xs text-base-content/20"
|
|
>{inactiveMissing.length}</span
|
|
>
|
|
</div>
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 opacity-40">
|
|
{#each inactiveMissing as tool}
|
|
<a
|
|
href="/tools/{tool.name}"
|
|
class="group hover:opacity-100 transition-opacity"
|
|
>
|
|
{@render toolCard(tool, true)}
|
|
</a>
|
|
{/each}
|
|
</div>
|
|
</section>
|
|
{/if}
|
|
|
|
{#if inactiveUnavail.length > 0}
|
|
<section>
|
|
<div class="flex items-center gap-2 mb-3">
|
|
<span class="size-1.5 rounded-full bg-base-content/20 shrink-0"
|
|
></span>
|
|
<span class="text-xs uppercase tracking-widest text-base-content/30"
|
|
>Disabled - unavailable</span
|
|
>
|
|
<span class="text-xs text-base-content/20"
|
|
>{inactiveUnavail.length}</span
|
|
>
|
|
</div>
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 opacity-40">
|
|
{#each inactiveUnavail as tool}
|
|
<a
|
|
href="/tools/{tool.name}"
|
|
class="group hover:opacity-100 transition-opacity"
|
|
>
|
|
{@render toolCard(tool, false)}
|
|
</a>
|
|
{/each}
|
|
</div>
|
|
</section>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
{/if}
|