Spaces:
Runtime error
Runtime error
Commit
·
029d885
1
Parent(s):
2e04298
Reroute model and persona pages to global settings modal.
Browse files
src/lib/components/NavMenu.svelte
CHANGED
|
@@ -75,6 +75,18 @@
|
|
| 75 |
|
| 76 |
const nModels: number = page.data.models.filter((el: Model) => !el.unlisted).length;
|
| 77 |
let nPersonas = $derived($settings.personas.length);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
|
| 79 |
async function handleVisible() {
|
| 80 |
p++;
|
|
@@ -180,7 +192,7 @@
|
|
| 180 |
</a>
|
| 181 |
{/if} -->
|
| 182 |
<a
|
| 183 |
-
href=
|
| 184 |
class="flex h-9 flex-none items-center gap-1.5 rounded-lg pl-2.5 pr-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
|
| 185 |
>
|
| 186 |
Models
|
|
@@ -191,7 +203,7 @@
|
|
| 191 |
</a>
|
| 192 |
|
| 193 |
<a
|
| 194 |
-
href=
|
| 195 |
class="flex h-9 flex-none items-center gap-1.5 rounded-lg pl-2.5 pr-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
|
| 196 |
>
|
| 197 |
Personas
|
|
|
|
| 75 |
|
| 76 |
const nModels: number = page.data.models.filter((el: Model) => !el.unlisted).length;
|
| 77 |
let nPersonas = $derived($settings.personas.length);
|
| 78 |
+
|
| 79 |
+
// Route to first active persona or first persona
|
| 80 |
+
let personasRoute = $derived.by(() => {
|
| 81 |
+
const targetId = $settings.activePersonas[0] || $settings.personas[0]?.id || '';
|
| 82 |
+
return `${base}/settings/personas/${targetId}`;
|
| 83 |
+
});
|
| 84 |
+
|
| 85 |
+
// Route to active model or first model
|
| 86 |
+
let modelsRoute = $derived.by(() => {
|
| 87 |
+
const targetId = $settings.activeModel || page.data.models[0]?.id || '';
|
| 88 |
+
return `${base}/settings/models/${targetId}`;
|
| 89 |
+
});
|
| 90 |
|
| 91 |
async function handleVisible() {
|
| 92 |
p++;
|
|
|
|
| 192 |
</a>
|
| 193 |
{/if} -->
|
| 194 |
<a
|
| 195 |
+
href={modelsRoute}
|
| 196 |
class="flex h-9 flex-none items-center gap-1.5 rounded-lg pl-2.5 pr-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
|
| 197 |
>
|
| 198 |
Models
|
|
|
|
| 203 |
</a>
|
| 204 |
|
| 205 |
<a
|
| 206 |
+
href={personasRoute}
|
| 207 |
class="flex h-9 flex-none items-center gap-1.5 rounded-lg pl-2.5 pr-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
|
| 208 |
>
|
| 209 |
Personas
|
src/lib/components/OverloadedModal.svelte
CHANGED
|
@@ -26,7 +26,7 @@
|
|
| 26 |
|
| 27 |
<div class="flex w-full flex-col items-center gap-4 pt-4">
|
| 28 |
<button
|
| 29 |
-
onclick={() => (onClose(), goto(`${base}/models`))}
|
| 30 |
class="flex w-full flex-wrap items-center justify-center whitespace-nowrap rounded-full border-2 border-black bg-black px-5 py-2 text-lg font-semibold text-gray-100 transition-colors hover:bg-gray-900"
|
| 31 |
>
|
| 32 |
See all available models
|
|
|
|
| 26 |
|
| 27 |
<div class="flex w-full flex-col items-center gap-4 pt-4">
|
| 28 |
<button
|
| 29 |
+
onclick={() => (onClose(), goto(`${base}/settings/models`))}
|
| 30 |
class="flex w-full flex-wrap items-center justify-center whitespace-nowrap rounded-full border-2 border-black bg-black px-5 py-2 text-lg font-semibold text-gray-100 transition-colors hover:bg-gray-900"
|
| 31 |
>
|
| 32 |
See all available models
|
src/routes/models/+page.svelte
DELETED
|
@@ -1,147 +0,0 @@
|
|
| 1 |
-
<script lang="ts">
|
| 2 |
-
import type { PageData } from "./$types";
|
| 3 |
-
import { usePublicConfig } from "$lib/utils/PublicConfig.svelte";
|
| 4 |
-
|
| 5 |
-
import { base } from "$app/paths";
|
| 6 |
-
import { page } from "$app/state";
|
| 7 |
-
|
| 8 |
-
import CarbonHelpFilled from "~icons/carbon/help-filled";
|
| 9 |
-
import CarbonView from "~icons/carbon/view";
|
| 10 |
-
import { useSettingsStore } from "$lib/stores/settings";
|
| 11 |
-
interface Props {
|
| 12 |
-
data: PageData;
|
| 13 |
-
}
|
| 14 |
-
|
| 15 |
-
let { data }: Props = $props();
|
| 16 |
-
|
| 17 |
-
const settings = useSettingsStore();
|
| 18 |
-
|
| 19 |
-
const publicConfig = usePublicConfig();
|
| 20 |
-
|
| 21 |
-
// Local filter state for model search (hyphen/space insensitive)
|
| 22 |
-
let modelFilter = $state("");
|
| 23 |
-
const normalize = (s: string) => s.toLowerCase().replace(/[^a-z0-9]+/g, " ");
|
| 24 |
-
let queryTokens = $derived(normalize(modelFilter).trim().split(/\s+/).filter(Boolean));
|
| 25 |
-
</script>
|
| 26 |
-
|
| 27 |
-
<svelte:head>
|
| 28 |
-
{#if publicConfig.isHuggingChat}
|
| 29 |
-
<title>HuggingChat - Models</title>
|
| 30 |
-
<meta property="og:title" content="HuggingChat - Models" />
|
| 31 |
-
<meta property="og:type" content="link" />
|
| 32 |
-
<meta property="og:description" content="Browse HuggingChat available models" />
|
| 33 |
-
<meta property="og:url" content={page.url.href} />
|
| 34 |
-
{/if}
|
| 35 |
-
</svelte:head>
|
| 36 |
-
|
| 37 |
-
<div class="scrollbar-custom h-full overflow-y-auto py-12 max-sm:pt-8 md:py-24">
|
| 38 |
-
<div class="pt-42 mx-auto flex flex-col px-5 xl:w-[60rem] 2xl:w-[64rem]">
|
| 39 |
-
<div class="flex items-center">
|
| 40 |
-
<h1 class="text-2xl font-bold">Models</h1>
|
| 41 |
-
{#if publicConfig.isHuggingChat}
|
| 42 |
-
<a
|
| 43 |
-
href="https://huggingface.co/spaces/huggingchat/chat-ui/discussions/372"
|
| 44 |
-
class="ml-auto dark:text-gray-400 dark:hover:text-gray-300"
|
| 45 |
-
target="_blank"
|
| 46 |
-
aria-label="Hub discussion about models"
|
| 47 |
-
>
|
| 48 |
-
<CarbonHelpFilled />
|
| 49 |
-
</a>
|
| 50 |
-
{/if}
|
| 51 |
-
</div>
|
| 52 |
-
<h2 class="text-gray-500">All models available on {publicConfig.PUBLIC_APP_NAME}</h2>
|
| 53 |
-
|
| 54 |
-
<!-- Filter input -->
|
| 55 |
-
<input
|
| 56 |
-
type="search"
|
| 57 |
-
bind:value={modelFilter}
|
| 58 |
-
placeholder="Search by name"
|
| 59 |
-
aria-label="Search models by name or id"
|
| 60 |
-
class="mt-4 w-full rounded-3xl border border-gray-300 bg-white px-5 py-2 text-[15px]
|
| 61 |
-
placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-300
|
| 62 |
-
dark:border-gray-700 dark:bg-gray-900 dark:focus:ring-gray-700"
|
| 63 |
-
/>
|
| 64 |
-
<div class="mt-6 grid grid-cols-1 gap-3 sm:gap-5 xl:grid-cols-2">
|
| 65 |
-
{#each data.models
|
| 66 |
-
.filter((el) => !el.unlisted)
|
| 67 |
-
.filter((el) => {
|
| 68 |
-
const haystack = normalize(`${el.id} ${el.name ?? ""} ${el.displayName ?? ""}`);
|
| 69 |
-
return queryTokens.every((q) => haystack.includes(q));
|
| 70 |
-
}) as model, index (model.id)}
|
| 71 |
-
<a
|
| 72 |
-
href="{base}/models/{model.id}"
|
| 73 |
-
aria-label="Model card"
|
| 74 |
-
class="relative flex flex-col gap-2 overflow-hidden rounded-xl border bg-gray-50/50 px-6 py-5 shadow hover:bg-gray-50 hover:shadow-inner dark:border-gray-800/70 dark:bg-gray-950/20 dark:hover:bg-gray-950/40"
|
| 75 |
-
class:active-model={model.id === $settings.activeModel}
|
| 76 |
-
>
|
| 77 |
-
<div class="flex items-center justify-between gap-1">
|
| 78 |
-
{#if model.logoUrl}
|
| 79 |
-
<img
|
| 80 |
-
class="aspect-square size-6 rounded border bg-white dark:border-gray-700"
|
| 81 |
-
src={model.logoUrl}
|
| 82 |
-
alt="{model.displayName} logo"
|
| 83 |
-
/>
|
| 84 |
-
{:else}
|
| 85 |
-
<div
|
| 86 |
-
class="size-6 rounded border border-transparent bg-gray-300 dark:bg-gray-800"
|
| 87 |
-
aria-hidden="true"
|
| 88 |
-
></div>
|
| 89 |
-
{/if}
|
| 90 |
-
<div class="flex items-center gap-1">
|
| 91 |
-
{#if $settings.multimodalOverrides?.[model.id] ?? model.multimodal}
|
| 92 |
-
<span
|
| 93 |
-
title="This model is multimodal and supports image inputs natively."
|
| 94 |
-
class="ml-auto flex size-[21px] items-center justify-center rounded-lg border border-blue-700 dark:border-blue-500"
|
| 95 |
-
aria-label="Model is multimodal"
|
| 96 |
-
role="img"
|
| 97 |
-
>
|
| 98 |
-
<CarbonView class="text-xxs text-blue-700 dark:text-blue-500" />
|
| 99 |
-
</span>
|
| 100 |
-
{/if}
|
| 101 |
-
{#if model.reasoning}
|
| 102 |
-
<span
|
| 103 |
-
title="This model supports reasoning."
|
| 104 |
-
class="ml-auto grid size-[21px] place-items-center rounded-lg border border-purple-300 dark:border-purple-700"
|
| 105 |
-
aria-label="Model supports reasoning"
|
| 106 |
-
role="img"
|
| 107 |
-
>
|
| 108 |
-
<svg
|
| 109 |
-
xmlns="http://www.w3.org/2000/svg"
|
| 110 |
-
width="14"
|
| 111 |
-
height="14"
|
| 112 |
-
viewBox="0 0 32 32"
|
| 113 |
-
>
|
| 114 |
-
<path
|
| 115 |
-
class="stroke-purple-700"
|
| 116 |
-
style="stroke-width: 2; fill: none; stroke-linecap: round; stroke-linejoin: round; stroke-dasharray: 50;"
|
| 117 |
-
d="M16 6v3.33M16 6c0-2.65 3.25-4.3 5.4-2.62 1.2.95 1.6 2.65.95 4.04a3.63 3.63 0 0 1 4.61.16 3.45 3.45 0 0 1 .46 4.37 5.32 5.32 0 0 1 1.87 4.75c-.22 1.66-1.39 3.6-3.07 4.14M16 6c0-2.65-3.25-4.3-5.4-2.62a3.37 3.37 0 0 0-.95 4.04 3.65 3.65 0 0 0-4.6.16 3.37 3.37 0 0 0-.49 4.27 5.57 5.57 0 0 0-1.85 4.85 5.3 5.3 0 0 0 3.07 4.15M16 9.33v17.34m0-17.34c0 2.18 1.82 4 4 4m6.22 7.5c.67 1.3.56 2.91-.27 4.11a4.05 4.05 0 0 1-4.62 1.5c0 1.53-1.05 2.9-2.66 2.9A2.7 2.7 0 0 1 16 26.66m10.22-5.83a4.05 4.05 0 0 0-3.55-2.17m-16.9 2.18a4.05 4.05 0 0 0 .28 4.1c1 1.44 2.92 2.09 4.59 1.5 0 1.52 1.12 2.88 2.7 2.88A2.7 2.7 0 0 0 16 26.67M5.78 20.85a4.04 4.04 0 0 1 3.55-2.18"
|
| 118 |
-
/>
|
| 119 |
-
</svg>
|
| 120 |
-
</span>
|
| 121 |
-
{/if}
|
| 122 |
-
{#if model.id === $settings.activeModel}
|
| 123 |
-
<span
|
| 124 |
-
class="rounded-full bg-black px-2 py-0.5 text-xs text-white dark:bg-white dark:text-black"
|
| 125 |
-
>
|
| 126 |
-
Active
|
| 127 |
-
</span>
|
| 128 |
-
{:else if index === 0 && model.id === "omni"}
|
| 129 |
-
<span
|
| 130 |
-
class="rounded-full border border-gray-300 px-2 py-0.5 text-xs text-gray-500 dark:border-gray-500 dark:text-gray-400"
|
| 131 |
-
>
|
| 132 |
-
Default
|
| 133 |
-
</span>
|
| 134 |
-
{/if}
|
| 135 |
-
</div>
|
| 136 |
-
</div>
|
| 137 |
-
<span class="flex items-center gap-2 font-semibold">
|
| 138 |
-
{model.displayName}
|
| 139 |
-
</span>
|
| 140 |
-
<span class="line-clamp-4 whitespace-pre-wrap text-sm text-gray-500 dark:text-gray-400">
|
| 141 |
-
{model.description || "-"}
|
| 142 |
-
</span>
|
| 143 |
-
</a>
|
| 144 |
-
{/each}
|
| 145 |
-
</div>
|
| 146 |
-
</div>
|
| 147 |
-
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/routes/personas/+page.svelte
DELETED
|
@@ -1,514 +0,0 @@
|
|
| 1 |
-
<script lang="ts">
|
| 2 |
-
import { usePublicConfig } from "$lib/utils/PublicConfig.svelte";
|
| 3 |
-
import { useSettingsStore } from "$lib/stores/settings";
|
| 4 |
-
import type { Persona } from "$lib/types/Persona";
|
| 5 |
-
import { page } from "$app/state";
|
| 6 |
-
import Modal from "$lib/components/Modal.svelte";
|
| 7 |
-
import CarbonEdit from "~icons/carbon/edit";
|
| 8 |
-
|
| 9 |
-
const publicConfig = usePublicConfig();
|
| 10 |
-
const settings = useSettingsStore();
|
| 11 |
-
|
| 12 |
-
let clickTimeout: number | null = null;
|
| 13 |
-
let showCloseConfirm = $state(false);
|
| 14 |
-
|
| 15 |
-
// Local filter state for persona search (hyphen/space insensitive)
|
| 16 |
-
let personaFilter = $state("");
|
| 17 |
-
const normalize = (s: string) => s.toLowerCase().replace(/[^a-z0-9]+/g, " ");
|
| 18 |
-
let queryTokens = $derived(normalize(personaFilter).trim().split(/\s+/).filter(Boolean));
|
| 19 |
-
let filtered = $derived(
|
| 20 |
-
$settings.personas.filter((p: Persona) => {
|
| 21 |
-
const haystack = normalize(`${p.name} ${p.age ?? ""} ${p.gender ?? ""} ${p.jobSector ?? ""} ${p.stance ?? ""}`);
|
| 22 |
-
return queryTokens.every((q) => haystack.includes(q));
|
| 23 |
-
})
|
| 24 |
-
);
|
| 25 |
-
|
| 26 |
-
function togglePersona(personaId: string) {
|
| 27 |
-
const isActive = $settings.activePersonas.includes(personaId);
|
| 28 |
-
if (isActive) {
|
| 29 |
-
// Prevent deactivating the last active persona
|
| 30 |
-
if ($settings.activePersonas.length === 1) {
|
| 31 |
-
alert("At least one persona must be active.");
|
| 32 |
-
return;
|
| 33 |
-
}
|
| 34 |
-
// Deactivate: remove from array
|
| 35 |
-
settings.instantSet({ activePersonas: $settings.activePersonas.filter(id => id !== personaId) });
|
| 36 |
-
} else {
|
| 37 |
-
// Activate: add to array
|
| 38 |
-
settings.instantSet({ activePersonas: [...$settings.activePersonas, personaId] });
|
| 39 |
-
}
|
| 40 |
-
}
|
| 41 |
-
|
| 42 |
-
function handleCardClick(personaId: string) {
|
| 43 |
-
if (clickTimeout) return; // waiting for dblclick
|
| 44 |
-
clickTimeout = window.setTimeout(() => {
|
| 45 |
-
clickTimeout = null;
|
| 46 |
-
openEdit(personaId);
|
| 47 |
-
}, 220);
|
| 48 |
-
}
|
| 49 |
-
|
| 50 |
-
function handleCardDblClick(personaId: string) {
|
| 51 |
-
if (clickTimeout) {
|
| 52 |
-
clearTimeout(clickTimeout);
|
| 53 |
-
clickTimeout = null;
|
| 54 |
-
}
|
| 55 |
-
togglePersona(personaId);
|
| 56 |
-
}
|
| 57 |
-
|
| 58 |
-
// Edit modal state
|
| 59 |
-
let editingPersonaId = $state<string | null>(null);
|
| 60 |
-
let editingPersona = $derived(
|
| 61 |
-
$settings.personas.find((p) => p.id === editingPersonaId) ?? null
|
| 62 |
-
);
|
| 63 |
-
let editableName = $state("");
|
| 64 |
-
let editableAge = $state("");
|
| 65 |
-
let editableGender = $state("");
|
| 66 |
-
let editableJobSector = $state("");
|
| 67 |
-
let editableStance = $state("");
|
| 68 |
-
let editableCommunicationStyle = $state("");
|
| 69 |
-
let editableGoalInDebate = $state("");
|
| 70 |
-
let editableIncomeBracket = $state("");
|
| 71 |
-
let editablePoliticalLeanings = $state("");
|
| 72 |
-
let editableGeographicContext = $state("");
|
| 73 |
-
|
| 74 |
-
$effect(() => {
|
| 75 |
-
if (editingPersona) {
|
| 76 |
-
editableName = editingPersona.name;
|
| 77 |
-
editableAge = editingPersona.age;
|
| 78 |
-
editableGender = editingPersona.gender;
|
| 79 |
-
editableJobSector = editingPersona.jobSector || "";
|
| 80 |
-
editableStance = editingPersona.stance || "";
|
| 81 |
-
editableCommunicationStyle = editingPersona.communicationStyle || "";
|
| 82 |
-
editableGoalInDebate = editingPersona.goalInDebate || "";
|
| 83 |
-
editableIncomeBracket = editingPersona.incomeBracket || "";
|
| 84 |
-
editablePoliticalLeanings = editingPersona.politicalLeanings || "";
|
| 85 |
-
editableGeographicContext = editingPersona.geographicContext || "";
|
| 86 |
-
}
|
| 87 |
-
});
|
| 88 |
-
|
| 89 |
-
function openEdit(personaId: string) {
|
| 90 |
-
editingPersonaId = personaId;
|
| 91 |
-
}
|
| 92 |
-
|
| 93 |
-
function closeEdit() {
|
| 94 |
-
editingPersonaId = null;
|
| 95 |
-
}
|
| 96 |
-
|
| 97 |
-
function saveEdit() {
|
| 98 |
-
if (!editingPersona) return;
|
| 99 |
-
$settings.personas = $settings.personas.map((p) =>
|
| 100 |
-
p.id === editingPersona.id
|
| 101 |
-
? {
|
| 102 |
-
...p,
|
| 103 |
-
name: editableName,
|
| 104 |
-
age: editableAge,
|
| 105 |
-
gender: editableGender,
|
| 106 |
-
jobSector: editableJobSector,
|
| 107 |
-
stance: editableStance,
|
| 108 |
-
communicationStyle: editableCommunicationStyle,
|
| 109 |
-
goalInDebate: editableGoalInDebate,
|
| 110 |
-
incomeBracket: editableIncomeBracket,
|
| 111 |
-
politicalLeanings: editablePoliticalLeanings,
|
| 112 |
-
geographicContext: editableGeographicContext,
|
| 113 |
-
updatedAt: new Date(),
|
| 114 |
-
}
|
| 115 |
-
: p
|
| 116 |
-
);
|
| 117 |
-
closeEdit();
|
| 118 |
-
}
|
| 119 |
-
|
| 120 |
-
function toggleEditingPersona() {
|
| 121 |
-
if (!editingPersona) return;
|
| 122 |
-
const id = editingPersona.id;
|
| 123 |
-
saveEdit();
|
| 124 |
-
togglePersona(id);
|
| 125 |
-
}
|
| 126 |
-
|
| 127 |
-
function deleteEditingPersona() {
|
| 128 |
-
if (!editingPersona) return;
|
| 129 |
-
if ($settings.personas.length === 1) return alert("Cannot delete the last persona.");
|
| 130 |
-
if ($settings.activePersonas.includes(editingPersona.id))
|
| 131 |
-
return alert("Cannot delete an active persona. Deactivate it first.");
|
| 132 |
-
if (confirm(`Delete "${editingPersona.name}"?`)) {
|
| 133 |
-
$settings.personas = $settings.personas.filter((p) => p.id !== editingPersona!.id);
|
| 134 |
-
closeEdit();
|
| 135 |
-
}
|
| 136 |
-
}
|
| 137 |
-
|
| 138 |
-
// Function to show datalist on focus
|
| 139 |
-
function showDatalist(event: FocusEvent) {
|
| 140 |
-
const input = event.target as HTMLInputElement;
|
| 141 |
-
// Dispatch a synthetic input event to trigger the datalist dropdown
|
| 142 |
-
input.dispatchEvent(new Event('input', { bubbles: true }));
|
| 143 |
-
}
|
| 144 |
-
</script>
|
| 145 |
-
|
| 146 |
-
<svelte:head>
|
| 147 |
-
<title>{publicConfig.PUBLIC_APP_NAME} - Personas</title>
|
| 148 |
-
<meta property="og:title" content="Personas" />
|
| 149 |
-
<meta property="og:url" content={page.url.href} />
|
| 150 |
-
</svelte:head>
|
| 151 |
-
|
| 152 |
-
<div class="scrollbar-custom h-full overflow-y-auto py-12 max-sm:pt-8 md:py-24">
|
| 153 |
-
<div class="pt-42 mx-auto flex flex-col px-5 xl:w-[60rem] 2xl:w-[64rem]">
|
| 154 |
-
<div class="flex items-center">
|
| 155 |
-
<h1 class="text-2xl font-bold">Personas</h1>
|
| 156 |
-
</div>
|
| 157 |
-
<h2 class="text-gray-500">All personas available on {publicConfig.PUBLIC_APP_NAME}</h2>
|
| 158 |
-
|
| 159 |
-
<!-- Filter input -->
|
| 160 |
-
<input
|
| 161 |
-
type="search"
|
| 162 |
-
bind:value={personaFilter}
|
| 163 |
-
placeholder="Search by name, age, gender, job sector, or stance"
|
| 164 |
-
aria-label="Search personas"
|
| 165 |
-
class="mt-4 w-full rounded-3xl border border-gray-300 bg-white px-5 py-2 text-[15px]
|
| 166 |
-
placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-300
|
| 167 |
-
dark:border-gray-700 dark:bg-gray-900 dark:focus:ring-gray-700"
|
| 168 |
-
/>
|
| 169 |
-
|
| 170 |
-
<div class="mt-6 grid grid-cols-1 gap-3 sm:gap-5 xl:grid-cols-2">
|
| 171 |
-
{#each filtered as persona (persona.id)}
|
| 172 |
-
<div
|
| 173 |
-
role="button"
|
| 174 |
-
tabindex="0"
|
| 175 |
-
onclick={() => handleCardClick(persona.id)}
|
| 176 |
-
ondblclick={() => handleCardDblClick(persona.id)}
|
| 177 |
-
onkeydown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); handleCardClick(persona.id); } }}
|
| 178 |
-
aria-label={`Open persona ${persona.name}`}
|
| 179 |
-
class="group relative flex min-h-[112px] flex-col gap-2 overflow-hidden rounded-xl bg-gray-50/50 px-6 py-5 text-left shadow hover:bg-gray-50 hover:shadow-inner dark:bg-gray-950/20 dark:hover:bg-gray-950/40 {$settings.activePersonas.includes(persona.id) ? 'border-2 border-black dark:border-white' : 'border border-gray-800/70'}"
|
| 180 |
-
>
|
| 181 |
-
<div class="flex items-center justify-between gap-1">
|
| 182 |
-
<span class="flex items-center gap-2 font-semibold">{persona.name}</span>
|
| 183 |
-
{#if $settings.activePersonas.includes(persona.id)}
|
| 184 |
-
<div class="size-2.5 rounded-full bg-black dark:bg-white" title="Active persona"></div>
|
| 185 |
-
{/if}
|
| 186 |
-
</div>
|
| 187 |
-
<div class="text-sm text-gray-600 dark:text-gray-300">
|
| 188 |
-
{#if persona.age || persona.gender}
|
| 189 |
-
<div class="mb-1">
|
| 190 |
-
{#if persona.age}<span>{persona.age}</span>{/if}
|
| 191 |
-
{#if persona.age && persona.gender}<span class="mx-1 text-gray-400">•</span>{/if}
|
| 192 |
-
{#if persona.gender}<span>{persona.gender}</span>{/if}
|
| 193 |
-
</div>
|
| 194 |
-
{/if}
|
| 195 |
-
{#if persona.jobSector || persona.stance}
|
| 196 |
-
<div>
|
| 197 |
-
{#if persona.jobSector}<span class="font-medium">{persona.jobSector}</span>{/if}
|
| 198 |
-
{#if persona.jobSector && persona.stance}<span class="mx-1 text-gray-400">•</span>{/if}
|
| 199 |
-
{#if persona.stance}<span class="italic">{persona.stance}</span>{/if}
|
| 200 |
-
</div>
|
| 201 |
-
{/if}
|
| 202 |
-
</div>
|
| 203 |
-
</div>
|
| 204 |
-
{/each}
|
| 205 |
-
</div>
|
| 206 |
-
</div>
|
| 207 |
-
</div>
|
| 208 |
-
|
| 209 |
-
{#if editingPersona}
|
| 210 |
-
<Modal onclose={() => {
|
| 211 |
-
const dirty = editingPersona && (
|
| 212 |
-
editableName !== editingPersona.name ||
|
| 213 |
-
editableAge !== editingPersona.age ||
|
| 214 |
-
editableGender !== editingPersona.gender ||
|
| 215 |
-
editableJobSector !== (editingPersona.jobSector || "") ||
|
| 216 |
-
editableStance !== (editingPersona.stance || "") ||
|
| 217 |
-
editableCommunicationStyle !== (editingPersona.communicationStyle || "") ||
|
| 218 |
-
editableGoalInDebate !== (editingPersona.goalInDebate || "") ||
|
| 219 |
-
editableIncomeBracket !== (editingPersona.incomeBracket || "") ||
|
| 220 |
-
editablePoliticalLeanings !== (editingPersona.politicalLeanings || "") ||
|
| 221 |
-
editableGeographicContext !== (editingPersona.geographicContext || "")
|
| 222 |
-
);
|
| 223 |
-
if (!dirty) return closeEdit();
|
| 224 |
-
showCloseConfirm = true;
|
| 225 |
-
}} width="w-full !max-w-4xl">
|
| 226 |
-
<div class="scrollbar-custom flex h-full max-h-[85vh] w-full flex-col gap-5 overflow-y-auto p-6">
|
| 227 |
-
<div class="text-xl font-semibold text-gray-800 dark:text-gray-200">Edit Persona</div>
|
| 228 |
-
|
| 229 |
-
<!-- Group 1: Core Identity -->
|
| 230 |
-
<div>
|
| 231 |
-
<h3 class="mb-3 text-sm font-semibold text-gray-800 dark:text-gray-200">Core Identity</h3>
|
| 232 |
-
<div class="grid grid-cols-1 gap-4 md:grid-cols-3 md:gap-6">
|
| 233 |
-
<div class="flex flex-col gap-2">
|
| 234 |
-
<label for="edit-name" class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
| 235 |
-
Name <span class="text-red-500">*</span>
|
| 236 |
-
</label>
|
| 237 |
-
<input
|
| 238 |
-
id="edit-name"
|
| 239 |
-
type="text"
|
| 240 |
-
bind:value={editableName}
|
| 241 |
-
required
|
| 242 |
-
class="w-full rounded-md border border-gray-300 bg-transparent px-3 py-2 text-sm transition-colors focus:bg-white focus:outline-none dark:border-gray-600 dark:focus:bg-gray-900"
|
| 243 |
-
maxlength="100"
|
| 244 |
-
/>
|
| 245 |
-
</div>
|
| 246 |
-
|
| 247 |
-
<div class="flex flex-col gap-2">
|
| 248 |
-
<label for="edit-age" class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
| 249 |
-
Age <span class="text-red-500">*</span>
|
| 250 |
-
</label>
|
| 251 |
-
<input
|
| 252 |
-
id="edit-age"
|
| 253 |
-
type="text"
|
| 254 |
-
list="edit-age-options"
|
| 255 |
-
bind:value={editableAge}
|
| 256 |
-
onfocus={showDatalist}
|
| 257 |
-
required
|
| 258 |
-
class="w-full rounded-md border border-gray-300 bg-transparent px-3 py-2 text-sm transition-colors focus:bg-white focus:outline-none dark:border-gray-600 dark:focus:bg-gray-900"
|
| 259 |
-
maxlength="50"
|
| 260 |
-
/>
|
| 261 |
-
<datalist id="edit-age-options">
|
| 262 |
-
<option value="18-25">18-25</option>
|
| 263 |
-
<option value="26-35">26-35</option>
|
| 264 |
-
<option value="36-45">36-45</option>
|
| 265 |
-
<option value="46-55">46-55</option>
|
| 266 |
-
<option value="56-65">56-65</option>
|
| 267 |
-
<option value="66+">66+</option>
|
| 268 |
-
</datalist>
|
| 269 |
-
</div>
|
| 270 |
-
|
| 271 |
-
<div class="flex flex-col gap-2">
|
| 272 |
-
<label for="edit-gender" class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
| 273 |
-
Gender <span class="text-red-500">*</span>
|
| 274 |
-
</label>
|
| 275 |
-
<input
|
| 276 |
-
id="edit-gender"
|
| 277 |
-
type="text"
|
| 278 |
-
list="edit-gender-options"
|
| 279 |
-
bind:value={editableGender}
|
| 280 |
-
onfocus={showDatalist}
|
| 281 |
-
required
|
| 282 |
-
class="w-full rounded-md border border-gray-300 bg-transparent px-3 py-2 text-sm transition-colors focus:bg-white focus:outline-none dark:border-gray-600 dark:focus:bg-gray-900"
|
| 283 |
-
maxlength="50"
|
| 284 |
-
/>
|
| 285 |
-
<datalist id="edit-gender-options">
|
| 286 |
-
<option value="Male">Male</option>
|
| 287 |
-
<option value="Female">Female</option>
|
| 288 |
-
<option value="Prefer not to say">Prefer not to say</option>
|
| 289 |
-
</datalist>
|
| 290 |
-
</div>
|
| 291 |
-
</div>
|
| 292 |
-
</div>
|
| 293 |
-
|
| 294 |
-
<!-- Group 2: Professional & Stance -->
|
| 295 |
-
<div>
|
| 296 |
-
<h3 class="mb-3 text-sm font-semibold text-gray-800 dark:text-gray-200">Professional & Stance</h3>
|
| 297 |
-
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 md:gap-6">
|
| 298 |
-
<div class="flex flex-col gap-2">
|
| 299 |
-
<label for="edit-job-sector" class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
| 300 |
-
Job Sector
|
| 301 |
-
</label>
|
| 302 |
-
<input
|
| 303 |
-
id="edit-job-sector"
|
| 304 |
-
type="text"
|
| 305 |
-
list="edit-job-sector-options"
|
| 306 |
-
bind:value={editableJobSector}
|
| 307 |
-
onfocus={showDatalist}
|
| 308 |
-
class="w-full rounded-md border border-gray-300 bg-transparent px-3 py-2 text-sm transition-colors focus:bg-white focus:outline-none dark:border-gray-600 dark:focus:bg-gray-900"
|
| 309 |
-
maxlength="200"
|
| 310 |
-
/>
|
| 311 |
-
<datalist id="edit-job-sector-options">
|
| 312 |
-
<option value="Healthcare provider">Healthcare provider</option>
|
| 313 |
-
<option value="Small business owner">Small business owner</option>
|
| 314 |
-
<option value="Tech worker">Tech worker</option>
|
| 315 |
-
<option value="Teacher">Teacher</option>
|
| 316 |
-
<option value="Unemployed/Retired">Unemployed/Retired</option>
|
| 317 |
-
<option value="Government worker">Government worker</option>
|
| 318 |
-
<option value="Student">Student</option>
|
| 319 |
-
</datalist>
|
| 320 |
-
</div>
|
| 321 |
-
|
| 322 |
-
<div class="flex flex-col gap-2">
|
| 323 |
-
<label for="edit-stance" class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
| 324 |
-
Stance
|
| 325 |
-
</label>
|
| 326 |
-
<input
|
| 327 |
-
id="edit-stance"
|
| 328 |
-
type="text"
|
| 329 |
-
list="edit-stance-options"
|
| 330 |
-
bind:value={editableStance}
|
| 331 |
-
onfocus={showDatalist}
|
| 332 |
-
class="w-full rounded-md border border-gray-300 bg-transparent px-3 py-2 text-sm transition-colors focus:bg-white focus:outline-none dark:border-gray-600 dark:focus:bg-gray-900"
|
| 333 |
-
maxlength="200"
|
| 334 |
-
/>
|
| 335 |
-
<datalist id="edit-stance-options">
|
| 336 |
-
<option value="In Favor of Medicare for All">In Favor of Medicare for All</option>
|
| 337 |
-
<option value="Hardline Insurance Advocate">Hardline Insurance Advocate</option>
|
| 338 |
-
<option value="Improvement of Current System">Improvement of Current System</option>
|
| 339 |
-
<option value="Public Option Supporter">Public Option Supporter</option>
|
| 340 |
-
<option value="Status Quo">Status Quo</option>
|
| 341 |
-
</datalist>
|
| 342 |
-
</div>
|
| 343 |
-
</div>
|
| 344 |
-
</div>
|
| 345 |
-
|
| 346 |
-
<!-- Group 3: Communication & Goals -->
|
| 347 |
-
<div>
|
| 348 |
-
<h3 class="mb-3 text-sm font-semibold text-gray-800 dark:text-gray-200">Communication & Goals</h3>
|
| 349 |
-
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 md:gap-6">
|
| 350 |
-
<div class="flex flex-col gap-2">
|
| 351 |
-
<label for="edit-communication-style" class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
| 352 |
-
Communication Style
|
| 353 |
-
</label>
|
| 354 |
-
<input
|
| 355 |
-
id="edit-communication-style"
|
| 356 |
-
type="text"
|
| 357 |
-
list="edit-communication-style-options"
|
| 358 |
-
bind:value={editableCommunicationStyle}
|
| 359 |
-
onfocus={showDatalist}
|
| 360 |
-
class="w-full rounded-md border border-gray-300 bg-transparent px-3 py-2 text-sm transition-colors focus:bg-white focus:outline-none dark:border-gray-600 dark:focus:bg-gray-900"
|
| 361 |
-
maxlength="200"
|
| 362 |
-
/>
|
| 363 |
-
<datalist id="edit-communication-style-options">
|
| 364 |
-
<option value="Direct">Direct</option>
|
| 365 |
-
<option value="Technical/Jargon use">Technical/Jargon use</option>
|
| 366 |
-
<option value="Informal">Informal</option>
|
| 367 |
-
<option value="Philosophical">Philosophical</option>
|
| 368 |
-
<option value="Pragmatic">Pragmatic</option>
|
| 369 |
-
<option value="Conversational">Conversational</option>
|
| 370 |
-
</datalist>
|
| 371 |
-
</div>
|
| 372 |
-
|
| 373 |
-
<div class="flex flex-col gap-2">
|
| 374 |
-
<label for="edit-goal-in-debate" class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
| 375 |
-
Goal in the Debate
|
| 376 |
-
</label>
|
| 377 |
-
<input
|
| 378 |
-
id="edit-goal-in-debate"
|
| 379 |
-
type="text"
|
| 380 |
-
list="edit-goal-in-debate-options"
|
| 381 |
-
bind:value={editableGoalInDebate}
|
| 382 |
-
onfocus={showDatalist}
|
| 383 |
-
class="w-full rounded-md border border-gray-300 bg-transparent px-3 py-2 text-sm transition-colors focus:bg-white focus:outline-none dark:border-gray-600 dark:focus:bg-gray-900"
|
| 384 |
-
maxlength="300"
|
| 385 |
-
/>
|
| 386 |
-
<datalist id="edit-goal-in-debate-options">
|
| 387 |
-
<option value="Keep discussion grounded">Keep discussion grounded</option>
|
| 388 |
-
<option value="Explain complexity">Explain complexity</option>
|
| 389 |
-
<option value="Advocate for change">Advocate for change</option>
|
| 390 |
-
<option value="Defend current system">Defend current system</option>
|
| 391 |
-
<option value="Find compromise">Find compromise</option>
|
| 392 |
-
</datalist>
|
| 393 |
-
</div>
|
| 394 |
-
</div>
|
| 395 |
-
</div>
|
| 396 |
-
|
| 397 |
-
<!-- Group 4: Demographics -->
|
| 398 |
-
<div>
|
| 399 |
-
<h3 class="mb-3 text-sm font-semibold text-gray-800 dark:text-gray-200">Demographics</h3>
|
| 400 |
-
<div class="grid grid-cols-1 gap-4 md:grid-cols-3 md:gap-6">
|
| 401 |
-
<div class="flex flex-col gap-2">
|
| 402 |
-
<label for="edit-income-bracket" class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
| 403 |
-
Income Bracket
|
| 404 |
-
</label>
|
| 405 |
-
<input
|
| 406 |
-
id="edit-income-bracket"
|
| 407 |
-
type="text"
|
| 408 |
-
list="edit-income-bracket-options"
|
| 409 |
-
bind:value={editableIncomeBracket}
|
| 410 |
-
onfocus={showDatalist}
|
| 411 |
-
class="w-full rounded-md border border-gray-300 bg-transparent px-3 py-2 text-sm transition-colors focus:bg-white focus:outline-none dark:border-gray-600 dark:focus:bg-gray-900"
|
| 412 |
-
maxlength="100"
|
| 413 |
-
/>
|
| 414 |
-
<datalist id="edit-income-bracket-options">
|
| 415 |
-
<option value="Low">Low</option>
|
| 416 |
-
<option value="Middle">Middle</option>
|
| 417 |
-
<option value="High">High</option>
|
| 418 |
-
<option value="Comfortable">Comfortable</option>
|
| 419 |
-
<option value="Struggling">Struggling</option>
|
| 420 |
-
</datalist>
|
| 421 |
-
</div>
|
| 422 |
-
|
| 423 |
-
<div class="flex flex-col gap-2">
|
| 424 |
-
<label for="edit-political-leanings" class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
| 425 |
-
Political Leanings
|
| 426 |
-
</label>
|
| 427 |
-
<input
|
| 428 |
-
id="edit-political-leanings"
|
| 429 |
-
type="text"
|
| 430 |
-
list="edit-political-leanings-options"
|
| 431 |
-
bind:value={editablePoliticalLeanings}
|
| 432 |
-
onfocus={showDatalist}
|
| 433 |
-
class="w-full rounded-md border border-gray-300 bg-transparent px-3 py-2 text-sm transition-colors focus:bg-white focus:outline-none dark:border-gray-600 dark:focus:bg-gray-900"
|
| 434 |
-
maxlength="100"
|
| 435 |
-
/>
|
| 436 |
-
<datalist id="edit-political-leanings-options">
|
| 437 |
-
<option value="Liberal">Liberal</option>
|
| 438 |
-
<option value="Conservative">Conservative</option>
|
| 439 |
-
<option value="Moderate">Moderate</option>
|
| 440 |
-
<option value="Libertarian">Libertarian</option>
|
| 441 |
-
<option value="Non-affiliated">Non-affiliated</option>
|
| 442 |
-
<option value="Progressive">Progressive</option>
|
| 443 |
-
</datalist>
|
| 444 |
-
</div>
|
| 445 |
-
|
| 446 |
-
<div class="flex flex-col gap-2">
|
| 447 |
-
<label for="edit-geographic-context" class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
| 448 |
-
Geographic Context
|
| 449 |
-
</label>
|
| 450 |
-
<input
|
| 451 |
-
id="edit-geographic-context"
|
| 452 |
-
type="text"
|
| 453 |
-
list="edit-geographic-context-options"
|
| 454 |
-
bind:value={editableGeographicContext}
|
| 455 |
-
onfocus={showDatalist}
|
| 456 |
-
class="w-full rounded-md border border-gray-300 bg-transparent px-3 py-2 text-sm transition-colors focus:bg-white focus:outline-none dark:border-gray-600 dark:focus:bg-gray-900"
|
| 457 |
-
maxlength="100"
|
| 458 |
-
/>
|
| 459 |
-
<datalist id="edit-geographic-context-options">
|
| 460 |
-
<option value="Rural">Rural</option>
|
| 461 |
-
<option value="Urban">Urban</option>
|
| 462 |
-
<option value="Suburban">Suburban</option>
|
| 463 |
-
</datalist>
|
| 464 |
-
</div>
|
| 465 |
-
</div>
|
| 466 |
-
</div>
|
| 467 |
-
|
| 468 |
-
<div class="flex flex-wrap gap-2 pt-2">
|
| 469 |
-
<button
|
| 470 |
-
class="rounded-lg border border-gray-300 bg-white px-4 py-2 text-sm font-semibold text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
|
| 471 |
-
onclick={toggleEditingPersona}
|
| 472 |
-
>
|
| 473 |
-
{$settings.activePersonas.includes(editingPersona.id) ? "Deactivate" : "Activate"}
|
| 474 |
-
</button>
|
| 475 |
-
<button
|
| 476 |
-
class="rounded-lg border border-gray-300 bg-white px-4 py-2 text-sm font-semibold text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
|
| 477 |
-
onclick={saveEdit}
|
| 478 |
-
>
|
| 479 |
-
Save
|
| 480 |
-
</button>
|
| 481 |
-
<button
|
| 482 |
-
class="ml-auto flex items-center gap-2 rounded-lg border border-red-300 bg-white px-4 py-2 text-sm font-semibold text-red-600 hover:bg-red-50 dark:border-red-700 dark:bg-gray-800 dark:hover:bg-red-900/20"
|
| 483 |
-
onclick={deleteEditingPersona}
|
| 484 |
-
>
|
| 485 |
-
Delete
|
| 486 |
-
</button>
|
| 487 |
-
</div>
|
| 488 |
-
</div>
|
| 489 |
-
</Modal>
|
| 490 |
-
{/if}
|
| 491 |
-
|
| 492 |
-
{#if editingPersona && showCloseConfirm}
|
| 493 |
-
<Modal onclose={() => (showCloseConfirm = false)} width="w-full !max-w-sm">
|
| 494 |
-
<div class="flex w-full flex-col gap-4 p-4">
|
| 495 |
-
<div class="text-base font-semibold text-gray-800 dark:text-gray-200">Unsaved changes</div>
|
| 496 |
-
<p class="text-sm text-gray-600 dark:text-gray-300">Save your changes before closing?</p>
|
| 497 |
-
<div class="mt-2 flex gap-2">
|
| 498 |
-
<button class="rounded-lg border border-gray-300 bg-white px-3 py-1.5 text-sm text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300" onclick={() => { showCloseConfirm = false; saveEdit(); }}>
|
| 499 |
-
Save
|
| 500 |
-
</button>
|
| 501 |
-
<button class="rounded-lg border border-gray-300 bg-white px-3 py-1.5 text-sm text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300" onclick={() => { showCloseConfirm = false; closeEdit(); }}>
|
| 502 |
-
Discard
|
| 503 |
-
</button>
|
| 504 |
-
<button class="ml-auto rounded-lg border border-gray-300 bg-white px-3 py-1.5 text-sm text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300" onclick={() => { showCloseConfirm = false; }}>
|
| 505 |
-
Cancel
|
| 506 |
-
</button>
|
| 507 |
-
</div>
|
| 508 |
-
</div>
|
| 509 |
-
</Modal>
|
| 510 |
-
{/if}
|
| 511 |
-
|
| 512 |
-
<!-- merged into top script -->
|
| 513 |
-
|
| 514 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|