Skip to content

Commit

Permalink
enhance: update landing page (#1976)
Browse files Browse the repository at this point in the history
Update +page.svelte

prettier

mork_borg.jpeg

Update +page.svelte

Update +page.svelte

Update app.css

placeholder
  • Loading branch information
ivyjeong13 authored Mar 7, 2025
1 parent f7af180 commit 2aea656
Show file tree
Hide file tree
Showing 8 changed files with 219 additions and 73 deletions.
20 changes: 18 additions & 2 deletions ui/user/src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
}

.icon-default {
@apply icon-default-size text-gray;
@apply icon-default-size;
}

body {
Expand All @@ -31,7 +31,7 @@ body {
}

.button {
@apply button-layout button-colors;
@apply button-layout button-colors transition-colors duration-200;
}

.button-secondary-colors {
Expand Down Expand Up @@ -124,6 +124,22 @@ html {
@apply bg-surface3 text-on-surface3;
}

.card {
@apply flex min-h-40 w-full gap-2 rounded-xl bg-surface1 transition-all duration-300 hover:-translate-y-2 hover:shadow-md;
}

.card-icon-button-colors {
@apply bg-gray-100/35 text-on-surface1 transition-all duration-300 hover:bg-gray-100/75 focus:outline-none dark:bg-gray-900/25 dark:hover:bg-gray-900/75;
}

.featured-card-layout {
@apply mb-8 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4;
}

.card-layout {
@apply mb-8 grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4 2xl:grid-cols-6;
}

textarea,
input {
@apply bg-background text-on-background;
Expand Down
37 changes: 37 additions & 0 deletions ui/user/src/lib/components/Search.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<script lang="ts">
import { SearchIcon } from 'lucide-svelte';
import { twMerge } from 'tailwind-merge';
interface Props {
onChange: (value: string) => void;
class?: string;
}
let { onChange, class: klass }: Props = $props();
let searchTimeout: ReturnType<typeof setTimeout>;
function search(e: Event) {
const value = (e.target as HTMLInputElement).value;
// Clear previous timeout
if (searchTimeout) clearTimeout(searchTimeout);
// Set new timeout for debounced search
searchTimeout = setTimeout(() => {
onChange(value);
}, 300);
}
</script>

<div class="relative mb-8 w-full">
<input
type="text"
placeholder="Search Obots..."
class={twMerge(
'peer w-full rounded-xl border-none bg-surface1 px-2.5 py-4 pl-12 ring-2 ring-transparent transition-all duration-200 hover:ring-2 hover:ring-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500',
klass
)}
oninput={search}
/>
<SearchIcon class="absolute left-4 top-1/2 -translate-y-1/2 text-gray peer-focus:text-blue-500" />
</div>
11 changes: 10 additions & 1 deletion ui/user/src/lib/services/chat/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ import {
type ProjectShareList,
type ProjectAuthorizationList,
type ProjectCredentialList,
type ProjectShare
type ProjectShare,
type ToolReferenceList
} from './types';

export type Fetcher = typeof fetch;
Expand Down Expand Up @@ -377,6 +378,14 @@ export async function createTool(
return result;
}

export async function listAllTools(opts?: { fetch: Fetcher }): Promise<ToolReferenceList> {
const list = (await doGet(`/tool-references?type=tool`, opts)) as ToolReferenceList;
if (!list.items) {
list.items = [];
}
return list;
}

export async function testTool(
assistantID: string,
projectID: string,
Expand Down
14 changes: 14 additions & 0 deletions ui/user/src/lib/services/chat/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,19 @@ export interface AssistantToolList {
items: AssistantTool[];
}

export interface ToolReference {
id: string;
name: string;
metadata?: {
icon?: string;
};
}

export interface ToolReferenceList {
readonly?: boolean;
items: ToolReference[];
}

export interface Credential {
toolName: string;
icon: string;
Expand Down Expand Up @@ -303,6 +316,7 @@ export interface Project {
introductionMessage?: string;
prompt?: string;
editor?: boolean;
tools?: string[];
}

export interface ProjectList {
Expand Down
203 changes: 135 additions & 68 deletions ui/user/src/routes/home/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
<script lang="ts">
import DarkModeToggle from '$lib/components/navbar/DarkModeToggle.svelte';
import { darkMode } from '$lib/stores';
import { Copy, ExternalLink, Trash2 } from 'lucide-svelte';
import { Copy, Trash2, WrenchIcon } from 'lucide-svelte';
import { Plus } from 'lucide-svelte/icons';
import Profile from '$lib/components/navbar/Profile.svelte';
import AssistantIcon from '$lib/icons/AssistantIcon.svelte';
import { ChatService } from '$lib/services';
import { ChatService, type ProjectShare } from '$lib/services';
import { errors } from '$lib/stores';
import { goto } from '$app/navigation';
import Notifications from '$lib/components/Notifications.svelte';
import type { PageProps } from './$types';
import DotDotDot from '$lib/components/DotDotDot.svelte';
import { type Project } from '$lib/services';
import Confirm from '$lib/components/Confirm.svelte';
import { twMerge } from 'tailwind-merge';
import Search from '$lib/components/Search.svelte';
let { data }: PageProps = $props();
let toDelete = $state<Project>();
let projects = $state(data.editorProjects);
let shares = $state<ProjectShare[]>(data.shares.filter((s) => !s.featured));
let featured = $state<ProjectShare[]>(data.shares.filter((s) => s.featured));
let tools = $state(new Map(data.tools.map((t) => [t.id, t])));
let searchResults = $state<(Project | ProjectShare)[]>([]);
let searchQuery = $state('');
async function createNew() {
const assistants = (await ChatService.listAssistants()).items;
Expand All @@ -37,6 +43,23 @@
const newProject = await ChatService.copyProject(project.assistantID, project.id);
projects.push(newProject);
}
function handleSearch(value: string) {
searchQuery = value;
searchResults = [...projects, ...shares, ...featured].filter(
(project) =>
project.name?.toLowerCase().includes(value.toLowerCase()) ||
project.description?.toLowerCase().includes(value.toLowerCase())
);
}
function getImage(project: Project | ProjectShare) {
const imageUrl = darkMode.isDark
? (project.icons?.iconDark ?? project.icons?.icon)
: project.icons?.icon;
return imageUrl ?? '/agent/images/placeholder.jpeg'; // need placeholder image
}
</script>

<div class="flex h-full flex-col items-center">
Expand All @@ -52,81 +75,121 @@
</div>

{#snippet menu(project: Project)}
<div class="absolute right-0.5 top-0.5">
<DotDotDot class="icon-button-colors min-h-10 min-w-10 rounded-full p-2.5 text-sm">
<div class="flex flex-col rounded-xl border-surface2 bg-surface1">
<button
class="flex items-center gap-2 rounded-t-xl px-4 py-2 hover:bg-surface3"
onclick={() => (toDelete = project)}
>
<Trash2 class="icon-default" />
<span>Delete</span>
</button>
<button
class="flex items-center gap-2 rounded-b-xl px-4 py-2 hover:bg-surface3"
onclick={() => copy(project)}
>
<Copy class="icon-default" />
<span>Copy</span>
</button>
</div>
</DotDotDot>
</div>
{/snippet}

<main class="colors-background container flex max-w-[1000px] flex-col justify-center p-5">
<div class="mt-24 flex w-full flex-col gap-5">
<div class="flex items-center justify-between">
<h3 class="text-2xl font-semibold">My Obots</h3>
</div>
<div class="flex flex-wrap gap-5 rounded-3xl">
{#each projects as project}
<a
href="/o/{project.id}"
class="button relative flex aspect-video w-48 flex-col items-center justify-center gap-2 rounded-3xl bg-surface1 p-5"
>
<div class="flex items-center gap-2">
<AssistantIcon {project} class="h-8 w-8" />
<span>{project.name || 'Untitled'}</span>
</div>
{#if project.description}
<p class="text-sm text-gray">This is a description</p>
{/if}
{@render menu(project)}
</a>
{/each}
<DotDotDot class="card-icon-button-colors min-h-10 min-w-10 rounded-full p-2.5 text-sm">
<div class="flex flex-col rounded-xl border-surface2 bg-surface1">
<button
class="button flex aspect-video w-48 items-center justify-center gap-2 rounded-3xl bg-surface1 p-5"
onclick={() => createNew()}
class="flex items-center gap-2 rounded-t-xl px-4 py-2 hover:bg-surface3"
onclick={() => (toDelete = project)}
>
<Plus class="h-5 w-5" />
<span class="text-lg">New Obot</span>
<Trash2 class="icon-default" />
<span>Delete</span>
</button>
<button
class="flex items-center gap-2 rounded-b-xl px-4 py-2 hover:bg-surface3"
onclick={() => copy(project)}
>
<Copy class="icon-default" />
<span>Copy</span>
</button>
</div>
</DotDotDot>
{/snippet}

{#if data.shares.length > 0}
<div class="mt-20 flex items-center justify-between">
<h3 class="text-2xl font-semibold">Featured</h3>
{#snippet projectCard(project: Project | ProjectShare)}
<a
href="/o/{'projectID' in project ? project.projectID : (project as Project).id}"
class="card relative z-20 flex-col overflow-hidden shadow-md"
>
<div class="absolute left-0 top-0 z-30 flex w-full items-center justify-end p-2">
<div class="flex items-center justify-end">
{#if 'id' in project}
{@render menu(project)}
{/if}
</div>
<div class="flex flex-col gap-2 rounded-3xl">
{#each data.shares as template}
<div class="flex w-full items-center gap-2 rounded-3xl bg-surface1 p-10 py-5">
<AssistantIcon project={template} class="h-8 w-8" />
<div>
<span>{template.name || 'Untitled'}</span>
{#if template.description}
<p class="text-sm text-gray">This is a description</p>
</div>
<div class="relative aspect-video">
<img
alt="obot logo"
src={getImage(project)}
class="absolute left-0 top-0 h-full w-full object-cover opacity-85"
/>
<div
class="absolute -bottom-0 left-0 z-10 h-2/4 w-full bg-gradient-to-b from-transparent via-transparent to-surface1 transition-colors duration-300"
></div>
</div>
<div class="flex h-full flex-col gap-2 px-4 py-2">
<h4 class="font-semibold">{project.name || 'Untitled'}</h4>
<p class="line-clamp-3 text-xs text-gray">{project.description}</p>

{#if 'tools' in project && project.tools}
<div class="mt-auto flex flex-wrap items-center justify-end gap-2">
{#each project.tools as tool}
{@const toolData = tools.get(tool)}
<div
class="flex w-fit items-center gap-1 rounded-2xl bg-surface2 p-2 transition-all duration-300"
>
{#if toolData?.metadata?.icon}
<img
alt={toolData.name || 'Unknown'}
src={toolData.metadata.icon}
class={twMerge(
'h-4 w-4',
toolData.metadata.icon.endsWith('.svg') && 'dark:invert'
)}
/>
{:else}
<WrenchIcon class="h-4 w-4" />
{/if}
</div>
<div class="grow"></div>
<a href="/s/{template.publicID}" class="button flex gap-2">
<ExternalLink class="h-5 w-5" />
Launch
</a>
</div>
{/each}
{/each}
</div>
{:else}
<div class="min-h-2"></div>
<!-- placeholder -->
{/if}
</div>
</a>
{/snippet}

<main class="colors-background flex w-full max-w-screen-2xl flex-col justify-center px-12 pb-12">
<div class="mt-8 flex w-full flex-col gap-8">
<Search onChange={handleSearch} />

{#if featured.length > 0 && !searchQuery}
<div class="flex w-full flex-col gap-4">
<h3 class="text-2xl font-semibold">Featured</h3>
<div class="featured-card-layout">
{#each featured as featuredShare}
{@render projectCard(featuredShare)}
{/each}
</div>
</div>
{/if}

<div class="flex w-full flex-col gap-4">
<h3 class="text-2xl font-semibold">My Obots</h3>
<div class="card-layout">
{#if searchQuery}
{#each searchResults as project}
{@render projectCard(project)}
{/each}
{#if searchResults.length === 0}
<p class="text-gray">No results found.</p>
{/if}
{:else}
{#each [...projects, ...shares] as project}
{@render projectCard(project)}
{/each}
<button
class="card flex items-center justify-center gap-1 shadow-md"
onclick={() => createNew()}
>
<Plus class="h-5 w-5" />
<span class="font-semibold">Create New Obot</span>
</button>
{/if}
</div>
</div>
</div>
</main>

Expand All @@ -147,3 +210,7 @@
}}
oncancel={() => (toDelete = undefined)}
/>

<svelte:head>
<title>Obot | Home</title>
</svelte:head>
Loading

0 comments on commit 2aea656

Please sign in to comment.