mirror of
https://github.com/anotherhadi/iknowyou.git
synced 2026-04-12 00:47:26 +02:00
init
This commit is contained in:
346
front/src/components/ToolList.svelte
Normal file
346
front/src/components/ToolList.svelte
Normal file
@@ -0,0 +1,346 @@
|
||||
<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}
|
||||
Reference in New Issue
Block a user