feat: add better provider support, icons, regen, and a lot more
This commit is contained in:
@@ -1,19 +1,34 @@
|
||||
# AGENTS.md
|
||||
# Veridian AGENTS.md
|
||||
|
||||
This file contains guidelines and commands for agentic coding agents working in
|
||||
the Veridian repository.
|
||||
**Nuxt 4 AI chat platform with Triplit database + better-auth**
|
||||
|
||||
## Project Overview
|
||||
## Architecture Overview
|
||||
|
||||
Veridian is a Nuxt 4 application built with TypeScript, using Triplit with
|
||||
better-auth for authentication, and UnoCSS for styling. The app is an agnetic
|
||||
chat platform that is meant to provide a well rounded experience for interacting
|
||||
with LLMs as well as providing a way to manage agents and provide helpful tools
|
||||
like RAG (retrieval augmented generation) and web scraping/search.
|
||||
```
|
||||
app/ # Nuxt 4 application (SSR)
|
||||
├── components/ # Vue components (Sidenav, Message, Settings, etc.)
|
||||
├── composables/ # Shared state (useChat, useAuth, useModels, etc.)
|
||||
├── layouts/ # Auth and default layouts
|
||||
├── middleware/ # Global auth middleware
|
||||
├── pages/ # File-based routing (/, /auth/*, /agent/*)
|
||||
├── plugins/ # Auth plugins (client/server), remark markdown
|
||||
├── types/ # TypeScript interfaces
|
||||
└── utils/ # Crypto, model-mapping, search utilities
|
||||
|
||||
You should be connected to the Nuxt docs MCP server, if you need to reference
|
||||
the documentation or are unsure on how to implement something, consult the
|
||||
docs.
|
||||
server/ # Server-side API routes
|
||||
└── api/
|
||||
├── auth/ # Better-auth handler
|
||||
├── chat/ # Generation, cancel endpoints
|
||||
└── provider/ # Model fetching from providers
|
||||
|
||||
triplit/ # Database
|
||||
├── schema.ts # Full schema with auth + app collections
|
||||
└── auth-schema.ts # Auth collections (users, sessions, accounts)
|
||||
|
||||
lib/ # Shared utilities
|
||||
├── auth.ts # Better-auth server config
|
||||
└── auth-client.ts # Better-auth client
|
||||
```
|
||||
|
||||
## Core Commands
|
||||
|
||||
@@ -26,215 +41,172 @@ database commands.
|
||||
|
||||
- `bunx triplit schema push` - Push schema changes to database
|
||||
|
||||
## Tech Stack & Dependencies
|
||||
## Code Style
|
||||
|
||||
- **Framework**: Nuxt 4 (SSR enabled)
|
||||
- **Language**: TypeScript with strict configuration
|
||||
- **Database**: Triplit (next generation fullstack syncing database, sort of
|
||||
like convex)
|
||||
- **Auth**: better-auth with email/password and social providers
|
||||
- **Styling**: UnoCSS with presetMini
|
||||
- **Icons**: @nuxt/icon with Iconify, use Myna UI Icons
|
||||
- **Indentation**: 4 spaces (no tabs)
|
||||
- **Semicolons**: Always required
|
||||
- **Equality**: Strict equality only (`===`, `!==`)
|
||||
- **Components**: PascalCase, never self-closing tags
|
||||
- **Functions**: camelCase for functions, PascalCase for constructors/classes
|
||||
- **Constants**: SCREAMING_SNAKE_CASE for constants, camelCase for const values
|
||||
- **Files**: kebab-case for files, PascalCase for Vue components
|
||||
- **State**: `useState('prefix:name', () => default)` for reactive state
|
||||
|
||||
## Code Style Guidelines
|
||||
## Imports Order
|
||||
|
||||
**Follow the LLVM golden rule: If you are extending, enhancing, or bug fixing
|
||||
already implemented code, use the style that is already being used so that
|
||||
the source is uniform and easy to follow.**
|
||||
Group imports in this order (alphabetical within groups):
|
||||
1. Type imports (`types/`)
|
||||
2. Dependency imports (npm packages)
|
||||
3. Triplit imports (`#triplit/`, `@triplit/`)
|
||||
4. Vue/Nuxt imports (`vue`, `#app`, `~~/`, `~/`, `@/`)
|
||||
5. Local imports (`lib/`, `server/`)
|
||||
|
||||
- **Always end lines with a semicolon regardless of surrounding code**
|
||||
- Use 4 spaces for indentation
|
||||
- Use consistent naming conventions for variables, functions, and constants
|
||||
- Always use strict equality checks (`===` and `!==`) instead of loose equality
|
||||
checks (`==` and `!=`)
|
||||
- Prefer using explicity equality checks over truthy/falsy checks
|
||||
e.g., prefer `if (value !== null)` instead of `if (value)`
|
||||
- Always check existing code patterns before implementing new features
|
||||
- Follow existing component and composable structures
|
||||
- Use proper TypeScript types for all function parameters and returns
|
||||
- Implement proper cleanup in composables (e.g., useClickOutside)
|
||||
- Prefer reactive state management over direct DOM manipulation
|
||||
- Keep components focused and single-purpose
|
||||
- Use semantic HTML elements where appropriate
|
||||
|
||||
### General Structure
|
||||
|
||||
- Use Nuxt's app directory structure (`app/`, `server/`, `lib/`)
|
||||
- Auth configuration in `lib/auth.ts`
|
||||
- Server API routes go in `server/api/`
|
||||
- Composables go in `app/composables/`
|
||||
- Database schema in `db/schema.ts`
|
||||
- Database migrations in `db/migrations/`
|
||||
|
||||
### TypeScript Guidelines
|
||||
|
||||
- Use strict TypeScript with no implicit any
|
||||
- Export types and interfaces explicitly
|
||||
- Use `computed()` and `ref()` from Vue 3 reactivity system
|
||||
- Type API responses and database models
|
||||
|
||||
### Vue Components
|
||||
|
||||
- **Never use self-closing tags, excluding images, br, hr, and input**
|
||||
- Use `<script setup lang="ts">` syntax
|
||||
- Strongly prefer composition API over options API
|
||||
- Prefer composables for shared state management
|
||||
- Maintain consistent 4-space indentation
|
||||
- Use PascalCase for component file names
|
||||
|
||||
### CSS/Styling
|
||||
|
||||
- Use UnoCSS utility classes exclusively
|
||||
- Never use margin for spacing, always prefer flexbox or grid
|
||||
- Prefer inline utility classes over custom CSS
|
||||
- Use semantic color variables: `--color-base`, `--color-neutral`, `--color-accent`, `--color-subtle`
|
||||
If you need to add new color variables, they are located in `app/assets/css/base.css`,
|
||||
along with a general CSS reset.
|
||||
- Apply consistent spacing and layout patterns
|
||||
- Use responsive prefixes only when necessary
|
||||
|
||||
### Imports
|
||||
|
||||
- Order imports: Vue/Nuxt imports first, then local files, then dependencies
|
||||
- `~/` is an alias for `app/` and `~~/` is an alias for the root of the project
|
||||
- Avoid wildcard imports
|
||||
- Keep imports sorted alphabetically within groups
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
- **Page**: camelCase
|
||||
- **Composables**: camelCase
|
||||
- **Components**: PascalCase when referenced in templates
|
||||
- **Functions/Variables**: camelCase
|
||||
- **Constants**: UPPER_SNAKE_CASE for environment variables only
|
||||
- **Database tables**: snake_case
|
||||
- **API routes**: kebab-case path segments
|
||||
|
||||
### Database Patterns
|
||||
|
||||
- Use Triplit for database operations
|
||||
- Export all schemas from `triplit/schema.ts`
|
||||
- Use environment variables for database credentials
|
||||
- Triplit schemas are defined in `triplit/schema.ts` and pushed using `bunx triplit schema push`
|
||||
|
||||
### Error Handling
|
||||
|
||||
- Use proper TypeScript error types
|
||||
- Implement try-catch blocks for database operations
|
||||
- Provide user-friendly error messages in API responses
|
||||
- Log errors appropriately without exposing sensitive data
|
||||
|
||||
### Performance Guidelines
|
||||
|
||||
- Leverage Nuxt's auto-imports and code splitting
|
||||
- Use `useState()` for shared state across components
|
||||
- Implement proper loading states with async operations
|
||||
- Optimize database queries and use indexes where needed
|
||||
|
||||
## Authentication Implementation
|
||||
|
||||
The app uses better-auth with:
|
||||
|
||||
- Email/password authentication (configurable via env vars)
|
||||
- Triplit adapter for authentication
|
||||
- Session management through cookies
|
||||
- Auth plugins in `app/plugins/` for client/server initialization
|
||||
- Global auth middleware in `app/middleware/auth.global.ts`
|
||||
|
||||
All users must be signed in to view pages aside from authentication pages.
|
||||
(located in `app/pages/auth/`), you do not need to check if session or user
|
||||
objects are null or undefined, use the non-null assertion operator (`!`) where
|
||||
necessary. In API routes, you can use `protectRoute` to guarantee that the
|
||||
user must be authenticated.
|
||||
|
||||
## File Organization
|
||||
|
||||
```
|
||||
|-- app/ # Application code
|
||||
│ |-- components/ # Vue components
|
||||
│ |-- composables/ # Reuseable composition functions
|
||||
│ |-- layouts/ # Layout components
|
||||
│ |-- middleware/ # Route middleware
|
||||
│ |-- pages/ # File-based routing
|
||||
│ \-- plugins/ # Vue/Nuxt plugins
|
||||
|-- server/ # Server-side code
|
||||
│ \-- api/ # API routes
|
||||
|-- triplit/ # Database related files
|
||||
│ |-- schema.ts # Database schema
|
||||
│ |-- client.ts # Interacts with the database on the client
|
||||
│ \-- server.ts # Interacts with the database on the server
|
||||
\-- lib/ # Shared utilities
|
||||
```typescript
|
||||
// Example import order
|
||||
import type { Result, Ok, Err } from '~~/types/result';
|
||||
import type { Entity } from '@triplit/client';
|
||||
import type schema from '#triplit/schema';
|
||||
import type { Foo } from 'vue';
|
||||
import { ref, computed } from 'vue';
|
||||
import { useFeature } from '~/composables/useFeature';
|
||||
import { decrypt } from '~/utils/crypto';
|
||||
import { authClient } from '~~/lib/auth-client';
|
||||
```
|
||||
|
||||
## Testing
|
||||
## Error Handling
|
||||
|
||||
Currently no test framework is configured, and tests are not currently a
|
||||
requirement.
|
||||
Avoid using `throw` statements in your code.
|
||||
|
||||
## Environment Variables
|
||||
Use the `Result<T, E>` type with `Ok()` and `Err()` factory functions:
|
||||
|
||||
Key environment variables:
|
||||
```typescript
|
||||
import { type Result, Ok, Err } from '~~/types/result';
|
||||
|
||||
- `TRIPLIT_SERVICE_TOKEN` - Triplit service token (admin token. **SECRET**)
|
||||
- `NUXT_TRIPLIT_ANON_TOKEN` - Triplit anonymous token (for anonymous access)
|
||||
- `BETTER_AUTH_SECRET` - Authentication secret (for better-auth)
|
||||
- `EXTERNAL_JWT_SECRET` - Will always be BETTER_AUTH_SECRET (used for
|
||||
verifying JWT tokens from better-auth)
|
||||
- `NUXT_PUBLIC_TRIPLIT_URL` - Triplit server URL
|
||||
const myFunction(): Result<ReturnType, ErrorType> {
|
||||
if (error) {
|
||||
return Err(ErrorType.SpecificError);
|
||||
}
|
||||
return Ok(data);
|
||||
}
|
||||
|
||||
## Common Patterns
|
||||
// Usage
|
||||
const result = myFunction();
|
||||
if (result.ok === false) {
|
||||
console.error(result.error);
|
||||
return;
|
||||
}
|
||||
processData(result.data);
|
||||
```
|
||||
|
||||
### Composables Pattern
|
||||
## Composables Pattern
|
||||
|
||||
All composables return reactive state + actions:
|
||||
|
||||
```typescript
|
||||
export const useFeature = () => {
|
||||
const state = useState<Type>('feature:state', () => defaultValue)
|
||||
const computed = computed(() => /* logic */)
|
||||
|
||||
const actions = {
|
||||
// methods
|
||||
}
|
||||
|
||||
return { state, computed, ...actions }
|
||||
}
|
||||
const state = useState('feature:state', () => defaultValue);
|
||||
const computedValue = computed(() => /* logic */);
|
||||
const actions = {
|
||||
async doSomething() {
|
||||
// implementation
|
||||
},
|
||||
};
|
||||
return { state, computedValue, ...actions };
|
||||
};
|
||||
```
|
||||
|
||||
### API Route Pattern
|
||||
|
||||
API routes are defined in `server/api/` and are exported as a single object.
|
||||
|
||||
File names contain information about the route, e.g. `user.get.ts` specifies a
|
||||
GET route to `/user`. This applies for all HTTP methods.
|
||||
## API Route Pattern
|
||||
|
||||
```typescript
|
||||
// an example GET route to /user
|
||||
// File: server/api/endpoint.method.ts
|
||||
import { protectRoute } from '~/server/utils/protect';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
await protectRoute(event);
|
||||
// implementation
|
||||
return { success: true, data: result };
|
||||
} catch (error) {
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: "Error description",
|
||||
});
|
||||
}
|
||||
return { /* response */ };
|
||||
});
|
||||
```
|
||||
|
||||
## Database Query Pattern (Triplit)
|
||||
|
||||
```typescript
|
||||
import { users } from "~~/db/schema";
|
||||
// Client queries use useQuery() with auto-includes
|
||||
useQuery('collection', triplit, triplit.query('collection').Include('relation'));
|
||||
|
||||
// an example POST route to /user
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody(event);
|
||||
|
||||
const { email, name, password } = body;
|
||||
|
||||
const [user] = await db
|
||||
.insert(users)
|
||||
.values({ email, name, password })
|
||||
.returning();
|
||||
|
||||
return user;
|
||||
});
|
||||
// Server-side
|
||||
import { httpClient } from '~~/server/lib/triplit';
|
||||
await httpClient.fetchOne(httpClient.query('collection').Where('id', '=', providerId));
|
||||
```
|
||||
|
||||
## Authentication Flow
|
||||
|
||||
1. **Login**: Client uses `authClient.signIn.email()` → derives encryption key from password
|
||||
2. **Session**: Better-auth creates session token → stored in cookie
|
||||
3. **Triplit**: Session token passed to `triplit.startSession(token)` for DB access
|
||||
4. **API Keys**: Encrypted client-side with AES-GCM (key derived from password + userId)
|
||||
|
||||
**Key files**: `lib/auth.ts`, `lib/auth-client.ts`, `app/plugins/auth.*.ts`, `app/middleware/auth.global.ts`
|
||||
|
||||
## Styling System
|
||||
|
||||
**UnoCSS** with semantic color variables in `app/assets/css/base.css`:
|
||||
- `--color-accent`, `--color-neutral`, `--color-text`, `--color-muted`
|
||||
- `--color-highlight` (for borders/hover states)
|
||||
- Theme-aware: `:root.dark` / `:root.light` with `color-mix()`
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Purpose |
|
||||
|----------|---------|
|
||||
| `TRIPLIT_SERVICE_TOKEN` | Admin DB access |
|
||||
| `NUXT_TRIPLIT_ANON_TOKEN` | Anonymous DB access |
|
||||
| `BETTER_AUTH_SECRET` | Auth encryption |
|
||||
| `NUXT_PUBLIC_TRIPLIT_URL` | DB server URL |
|
||||
|
||||
## Key Composables
|
||||
|
||||
| Composable | Purpose |
|
||||
|------------|---------|
|
||||
| `useAuth()` | Session/user state, signIn/signOut |
|
||||
| `useChat(id)` | Send/regenerate messages, create topics |
|
||||
| `useModels()` | Providers + models with auto-subscription |
|
||||
| `useAgents()` | User's agents with topics |
|
||||
| `useSidebar()` | Sidebar state + resize |
|
||||
| `useTheme()` | Accent/neutral theme cookies |
|
||||
| `useSettings()` | Settings dialog state |
|
||||
|
||||
## Key Routes
|
||||
|
||||
| Route | File |
|
||||
|-------|------|
|
||||
| `/` | `app/pages/index.vue` |
|
||||
| `/auth/login` | `app/pages/auth/login.vue` |
|
||||
| `/auth/register` | `app/pages/auth/register.vue` |
|
||||
| `/agent/:id` | `app/pages/agent/[id]/index.vue` |
|
||||
| `/agent/:id/topic/:topicId` | `app/pages/agent/[id]/topic/[topicId].vue` |
|
||||
| `/agent/:id/profile` | `app/pages/agent/[id]/profile.vue` |
|
||||
|
||||
## Server API Routes
|
||||
|
||||
| Endpoint | File |
|
||||
|----------|------|
|
||||
| `POST /api/chat/generate` | `server/api/chat/generate.post.ts` |
|
||||
| `POST /api/chat/cancel/:id` | `server/api/chat/cancel/[generationId].post.ts` |
|
||||
| `POST /api/provider/:id/models` | `server/api/provider/[providerId]/models.post.ts` |
|
||||
| `* /api/auth/*` | `server/api/auth/[...all].ts` |
|
||||
|
||||
## Known Issues (from BUGS.md)
|
||||
|
||||
1. Triplit occasionally makes duplicate connections (race condition)
|
||||
2. Sidebar hover animation occasionally glitches on agent routes
|
||||
3. Theme switcher + sidebar interaction bug
|
||||
|
||||
## Development Notes
|
||||
|
||||
- **Never run dev server as agent** - just complete tasks and signal done
|
||||
- Always run `bunx triplit schema push` after schema changes
|
||||
- Use `protectRoute()` in all API routes for auth
|
||||
- All UI state that persists → use cookies (`useCookie()`)
|
||||
- Real-time data → Triplit subscriptions via `useQuery()`
|
||||
- API keys → encrypted client-side before DB storage
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
I'm not entire sure why, I think its a race condition, but I'm not sure.
|
||||
I _do not_ think that triplit-nuxt is the culprit, but I'm not sure.
|
||||
|
||||
2. [ ] The logic to handle hovering over the sidebar is half-baked at best. On the
|
||||
2. [X] The logic to handle hovering over the sidebar is half-baked at best. On the
|
||||
agents route, the back arrow sometimes stays full sized
|
||||
|
||||
3. [ ] If you open the theme switcher on the sidenav then close it and re-open
|
||||
3. [X] If you open the theme switcher on the sidenav then close it and re-open
|
||||
it _without_ moving your mouse off of the sidenav, the agent button/dropdown
|
||||
trigger will slowly crawl to the right (likely related to #2).
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
|
||||
8. [ ] Sometimes the sidebar wont change views until you move your mouse off of the sidenav?
|
||||
|
||||
9. [ ] ~~Sometimes on first page load, the message send button is on the left?~~
|
||||
9. [x] ~~Sometimes on first page load, the message send button is on the left?~~
|
||||
If input was typed into the chat input **before hydation** the message send button
|
||||
will be on the left.
|
||||
will be on the left. **THIS WAS BECAUSE OF GRAMMARLY. I HATE YOU GRAMMARLY.**
|
||||
|
||||
10. [ ] Multiple markdown blocks might be edited with the same content at the same time
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 juls0730
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -37,6 +37,12 @@ if (import.meta.client) {
|
||||
document.documentElement.style.setProperty('--accent-hinting', `${hinting.value}%`);
|
||||
});
|
||||
}
|
||||
|
||||
useHead({
|
||||
bodyAttrs: {
|
||||
class: 'font-sans'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
--accent-volcano-hover: #dc2626;
|
||||
|
||||
/* neon lime */
|
||||
--accent-lime: #77fb6b;
|
||||
--accent-lime-hover: #5ee04f;
|
||||
--accent-lime: #29e154;
|
||||
--accent-lime-hover: #53e475;
|
||||
|
||||
/* electric sky */
|
||||
--accent-sky: #38d3fa;
|
||||
@@ -110,7 +110,6 @@ html,
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: var(--font-sans);
|
||||
background-color: var(--color-base);
|
||||
color: var(--color-text);
|
||||
}
|
||||
@@ -151,4 +150,8 @@ button.accent {
|
||||
|
||||
button.accent:hover {
|
||||
background-color: var(--color-accent-hover);
|
||||
}
|
||||
|
||||
.capitalize {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import type { ModelWithProvider, ProviderWithModels } from '~/composables/useMod
|
||||
import type { Entity } from '@triplit/client';
|
||||
import { schema } from '#triplit/schema';
|
||||
|
||||
const { allModels } = await useModels();
|
||||
|
||||
const inputRef = ref<HTMLTextAreaElement | null>(null);
|
||||
let tempInput = '';
|
||||
const inputValue = ref('');
|
||||
@@ -23,17 +25,6 @@ const props = defineProps<{
|
||||
// Model selection state
|
||||
const selectedModel = ref<ModelWithProvider | null>(null);
|
||||
|
||||
// Get all available models from all providers
|
||||
const allModels = computed(() => {
|
||||
if (!props.providers) return [];
|
||||
return props.providers.flatMap((provider) =>
|
||||
provider.models.map((model) => ({
|
||||
...model,
|
||||
provider,
|
||||
}))
|
||||
);
|
||||
});
|
||||
|
||||
// Initialize model selection based on agent's defaultModelId or first available
|
||||
const initializeModel = () => {
|
||||
if (selectedModel.value) return;
|
||||
@@ -147,11 +138,12 @@ onBeforeMount(() => {
|
||||
|
||||
onMounted(() => {
|
||||
inputValue.value = tempInput;
|
||||
handleInput();
|
||||
nextTick(() => {
|
||||
handleInput();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<div :class="['w-full flex max-h-full', $attrs.class]">
|
||||
<div class="relative w-full flex flex-shrink-1 flex-col gap-3 p-3 rounded-2xl border transition-border ease-in-out duration-300 bg-[var(--color-input)]
|
||||
@@ -167,10 +159,10 @@ onMounted(() => {
|
||||
</div>
|
||||
|
||||
<!-- Toolbar -->
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="flex-1">
|
||||
<ModelSelector v-if="providers && providers.length > 0" v-model="selectedModel"
|
||||
:providers="providers"></ModelSelector>
|
||||
<ModelSelector v-if="providers !== undefined" v-model="selectedModel" :providers="providers">
|
||||
</ModelSelector>
|
||||
</div>
|
||||
<!-- Send/Stop Button -->
|
||||
<button aria-label="Send message" @click="handleSubmit" :disabled="!inputValue.trim() && !loading"
|
||||
|
||||
@@ -90,7 +90,7 @@ useClickOutside(triggerRef, () => {
|
||||
<slot name="item-after" :item="item"></slot>
|
||||
</button>
|
||||
</template>
|
||||
<slot name="content"></slot>
|
||||
<slot name="content" :toggle="toggle"></slot>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
size?: string | number;
|
||||
color?: boolean;
|
||||
avatar?: boolean;
|
||||
}>();
|
||||
|
||||
const TITLE = 'Arcee';
|
||||
const AVATAR_SCALE = 0.7;
|
||||
|
||||
const BACKGROUND_COLOR = "#f0529c";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inline-flex items-center justify-center"
|
||||
:style="[`width: ${size}px; height: ${size}px;`, avatar ? `background-color: ${BACKGROUND_COLOR}; border-radius: 0.375rem;` : '']">
|
||||
<svg :style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
:fill="color && !avatar ? BACKGROUND_COLOR : 'currentColor'" fill-rule="evenodd"
|
||||
style="flex: none; line-height: 1;" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<path
|
||||
d="M9.553 9.378H4.777V4.835H8.62c.513 0 .932-.42.932-.932V.058h4.544v4.777a4.542 4.542 0 01-4.544 4.543zm-4.776.467H0v4.543h3.845c.512 0 .932.42.932.932v3.845H9.32v-4.777a4.542 4.542 0 00-4.543-4.543zM20.05 9.61a.935.935 0 01-.932-.932V4.835h-4.543V9.61a4.542 4.542 0 004.543 4.544h4.777V9.612H20.05zM9.787 19.166v4.777h4.544v-3.845c0-.513.42-.932.932-.932h3.845V14.62H14.33a4.542 4.542 0 00-4.544 4.544z" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
size?: string | number;
|
||||
color?: boolean;
|
||||
avatar?: boolean;
|
||||
}>();
|
||||
|
||||
const TITLE = 'AI21';
|
||||
const AVATAR_SCALE = 0.7;
|
||||
|
||||
const BACKGROUND_COLOR = "#E91E63";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inline-flex items-center justify-center"
|
||||
:style="[`width: ${size}px; height: ${size}px;`, avatar ? `background-color: ${BACKGROUND_COLOR}; border-radius: 0.375rem;` : '']">
|
||||
<svg :style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
fill="currentColor" fill-rule="evenodd" style="flex: none; line-height: 1;" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<path
|
||||
d="M6.47 17l-.367-1.189H2.718L2.35 17H0l3.398-9.789h2.026L8.864 17H6.47zm-2.052-6.993l-1.17 4.028H5.56l-1.142-4.028zm4.707-2.796h2.23V17h-2.23V7.211zM11.955 15c.1-.483.277-.946.524-1.37.214-.359.482-.68.795-.951.32-.273.658-.52 1.013-.741.28-.168.54-.33.781-.483.222-.14.433-.296.632-.468.172-.148.317-.325.428-.525.107-.199.16-.423.157-.65 0-.392-.104-.674-.313-.846a1.176 1.176 0 00-.775-.259 1.207 1.207 0 00-.863.329c-.231.219-.347.585-.347 1.098H11.8a3.387 3.387 0 01.224-1.245c.146-.377.371-.716.66-.993.306-.29.667-.514 1.06-.657A4.04 4.04 0 0115.183 7c.42-.002.84.057 1.244.175.376.107.73.287 1.04.531.305.246.55.562.714.923.185.419.275.875.265 1.335.005.39-.084.774-.259 1.12-.167.328-.38.63-.632.894-.246.259-.517.49-.808.693-.29.2-.554.37-.789.51-.326.224-.596.417-.809.58a3.872 3.872 0 00-.51.455 1.229 1.229 0 00-.265.434 1.633 1.633 0 00-.074.517h4.078V17h-6.606a9.24 9.24 0 01.183-2zM18.8 8.93a5.05 5.05 0 001.135-.105c.25-.049.484-.156.686-.314.163-.139.28-.324.34-.532.068-.25.1-.51.095-.77H23V17h-2.243v-6.475H18.8V8.93z" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
size?: string | number;
|
||||
color?: boolean;
|
||||
avatar?: boolean;
|
||||
}>();
|
||||
|
||||
const TITLE = 'Anthropic';
|
||||
const AVATAR_SCALE = 0.75;
|
||||
|
||||
const BACKGROUND_COLOR = "#F1F0E8";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inline-flex items-center justify-center"
|
||||
:style="[`width: ${size}px; height: ${size}px;`, avatar ? `background-color: ${BACKGROUND_COLOR}; border-radius: 0.375rem;` : '']">
|
||||
<svg :style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
:fill="avatar ? '#141413' : 'currentColor'" fill-rule="evenodd" style="flex: none; line-height: 1;"
|
||||
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<path
|
||||
d="M13.827 3.52h3.603L24 20h-3.603l-6.57-16.48zm-7.258 0h3.767L16.906 20h-3.674l-1.343-3.461H5.017l-1.344 3.46H0L6.57 3.522zm4.132 9.959L8.453 7.687 6.205 13.48H10.7z" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
size?: string | number;
|
||||
color?: boolean;
|
||||
avatar?: boolean;
|
||||
}>();
|
||||
|
||||
const TITLE = 'Arcee';
|
||||
const AVATAR_SCALE = 0.7;
|
||||
|
||||
const BACKGROUND_COLOR = "#008C8C";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inline-flex items-center justify-center"
|
||||
:style="[`width: ${size}px; height: ${size}px;`, avatar ? `background-color: ${BACKGROUND_COLOR}; border-radius: 0.375rem;` : '']">
|
||||
<svg :style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
:fill="color && !avatar ? BACKGROUND_COLOR : 'currentColor'" fill-rule="evenodd"
|
||||
style="flex: none; line-height: 1;" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<path
|
||||
d="M13.236 2.377L2.751 20.493H0L11.863 0l1.373 2.377zm3.554 6.156l-9.606 11.96H4.13L15.511 6.32l1.279 2.212zm6.908 11.96H14.05l8.406-2.151 1.242 2.15zm-3.42-5.922l-7.843 5.92H8.482l10.597-7.997 1.2 2.077z" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
size?: string | number;
|
||||
color?: boolean;
|
||||
avatar?: boolean;
|
||||
}>();
|
||||
|
||||
const TITLE = 'Aya';
|
||||
|
||||
const BACKGROUND_COLOR = "#416FDC";
|
||||
const AVATAR_SCALE = 0.6;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inline-flex items-center justify-center"
|
||||
:style="[`width: ${size}px; height: ${size}px;`, avatar ? `background-color: ${BACKGROUND_COLOR}; border-radius: 0.375rem;` : '']">
|
||||
<svg :style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
:fill="color && !avatar ? BACKGROUND_COLOR : 'currentColor'"
|
||||
:fill-rule="color && !avatar ? 'evenodd' : 'nonzero'" style="flex: none; line-height: 1;"
|
||||
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<path
|
||||
d="M19.066.131c-.064-.106-.212-.17-.34-.106l-2.39 1.333c-1.206.678-1.968 1.397-2.434 2.772a4.677 4.677 0 00.339 3.746c.063.106.212.17.338.106l2.392-1.333c1.206-.678 1.968-1.397 2.433-2.772A4.677 4.677 0 0019.066.13zM1.926 5.421a.325.325 0 00-.318.318c0 1.714.74 3.258 1.905 4.316C4.867 11.283 6.136 11.6 7.872 11.6H11.3c.169 0 .317-.148.317-.317 0-1.714-.74-3.26-1.904-4.317C8.358 5.739 7.089 5.42 5.353 5.42H1.927zM23.826 10.542a.325.325 0 00-.317-.317v.02h-3.47c-1.757 0-3.047.34-4.423 1.567-1.185 1.036-1.946 2.623-1.946 4.359 0 .169.148.317.317.317h3.47c1.757 0 3.047-.339 4.423-1.566a5.893 5.893 0 001.947-4.38zM0 15.79c0-.233.19-.445.444-.445h4.804c2.433 0 4.21.466 6.115 2.18 1.63 1.46 2.645 3.64 2.666 6.03 0 .233-.19.445-.444.445H8.782c-2.434 0-4.211-.444-6.116-2.158C1.036 20.36.021 18.18 0 15.79z" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
size?: string | number;
|
||||
color?: boolean;
|
||||
avatar?: boolean;
|
||||
}>();
|
||||
|
||||
const TITLE = 'BaiduCloud';
|
||||
|
||||
const BACKGROUND_COLOR = "#2468f2";
|
||||
const AVATAR_SCALE = 0.75;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inline-flex items-center justify-center"
|
||||
:style="[`width: ${size}px; height: ${size}px;`, avatar ? `background-color: ${BACKGROUND_COLOR}; border-radius: 0.375rem;` : '']">
|
||||
<svg :fill="avatar ? 'currentColor' : ''" fill-rule="evenodd" :height="size" style="flex: none; line-height: 1;"
|
||||
:style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
viewBox="0 0 24 24" :width="size" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<path :fill="!avatar && color ? '#5BCA87' : ''"
|
||||
d="M21.715 5.61l-3.983 2.31a.903.903 0 01-.896 0L12.44 5.384a.903.903 0 00-.897 0L7.156 7.92a.903.903 0 01-.896 0L2.276 5.617 12.002 0l9.713 5.61z" />
|
||||
<path :fill="!avatar && color ? '#5BCA87' : ''"
|
||||
d="M18.641 9.467a.89.89 0 00-.438.77v5.072a.896.896 0 01-.445.77l-4.428 2.51a.884.884 0 00-.445.777v4.607l4.429-2.536 5.31-3.047V7.157l-3.983 2.31z" />
|
||||
<path :fill="!avatar && color ? '#2468f2' : ''"
|
||||
d="M10.98 18.941a.936.936 0 00-.305-.352l-4.429-2.516a.903.903 0 01-.431-.764v-5.078a.89.89 0 00-.452-.757l-.451-.26L1.38 7.158V18.39l5.311 3.047L11.126 24v-4.608a.881.881 0 00-.146-.45z" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
size?: string | number;
|
||||
color?: boolean;
|
||||
avatar?: boolean;
|
||||
}>();
|
||||
|
||||
const TITLE = 'ByteDance';
|
||||
|
||||
const BACKGROUND_COLOR = "#325AB4";
|
||||
const AVATAR_SCALE = 0.6;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inline-flex items-center justify-center"
|
||||
:style="[`width: ${size}px; height: ${size}px;`, avatar ? `background-color: ${BACKGROUND_COLOR}; border-radius: 0.375rem;` : '']">
|
||||
<svg :fill="avatar ? 'currentColor' : ''" fill-rule="evenodd" :height="size" style="flex: none; line-height: 1;"
|
||||
:style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
viewBox="0 0 24 24" :width="size" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<path :fill="!avatar && color ? '#00C8D2' : ''" :fill-rule="!avatar && color ? 'nonzero' : 'evenodd'"
|
||||
d="M14.944 18.587l-1.704-.445V10.01l1.824-.462c1-.254 1.84-.461 1.88-.453.032 0 .056 2.235.056 4.972v4.973l-.176-.008c-.104 0-.952-.207-1.88-.446z" />
|
||||
<path :fill="!avatar && color ? '#3C8CFF' : ''" :fill-rule="!avatar && color ? 'nonzero' : 'evenodd'"
|
||||
d="M7 16.542c0-2.736.024-4.98.064-4.98.032-.008.872.2 1.88.454l1.816.461-.016 4.05-.024 4.049-1.632.422c-.896.23-1.736.445-1.856.469L7 21.523v-4.98z" />
|
||||
<path :fill="!avatar && color ? '#78E6DC' : ''" :fill-rule="!avatar && color ? 'nonzero' : 'evenodd'"
|
||||
d="M19.24 12.477c0-9.03.008-9.515.144-9.475.072.024.784.207 1.576.406.792.207 1.576.405 1.744.445l.296.08-.016 8.56-.024 8.568-1.624.414c-.888.23-1.728.437-1.856.47l-.24.055v-9.523z" />
|
||||
<path :fill="!avatar && color ? '#325AB4' : ''" :fill-rule="!avatar && color ? 'nonzero' : 'evenodd'"
|
||||
d="M1 12.509c0-4.678.024-8.505.064-8.505.032 0 .872.207 1.872.454l1.824.461v7.582c0 4.16-.016 7.574-.032 7.574-.024 0-.872.215-1.88.47L1 21.013v-8.505z" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
size?: string | number;
|
||||
color?: boolean;
|
||||
avatar?: boolean;
|
||||
}>();
|
||||
|
||||
const TITLE = 'ChatGLM';
|
||||
|
||||
const BACKGROUND_COLOR = "#4268FA";
|
||||
const AVATAR_SCALE = 0.75;
|
||||
|
||||
const [fill] = useFillIds(TITLE, 1);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inline-flex items-center justify-center"
|
||||
:style="[`width: ${size}px; height: ${size}px;`, avatar ? `background: ${BACKGROUND_COLOR}; border-radius: 0.375rem;` : '']">
|
||||
<svg v-if="color && !avatar"
|
||||
:style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<defs>
|
||||
<linearGradient :id="fill!.id" x1="-18.756%" x2="70.894%" y1="49.371%" y2="90.944%">
|
||||
<stop offset="0%" stop-color="#504AF4" />
|
||||
<stop offset="100%" stop-color="#3485FF" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path
|
||||
d="M9.917 2c4.906 0 10.178 3.947 8.93 10.58-.014.07-.037.14-.057.21l-.003-.277c-.083-3-1.534-8.934-8.87-8.934-3.393 0-8.137 3.054-7.93 8.158-.04 4.778 3.555 8.4 7.95 8.332l.073-.001c1.2-.033 2.763-.429 3.1-1.657.063-.031.26.534.268.598.048.256.112.369.192.34.981-.348 2.286-1.222 1.952-2.38-.176-.61-1.775-.147-1.921-.347.418-.979 2.234-.926 3.153-.716.443.102.657.38 1.012.442.29.052.981-.2.96.242-1.5 3.042-4.893 5.41-8.808 5.41C3.654 22 0 16.574 0 11.737 0 5.947 4.959 2 9.917 2zM9.9 5.3c.484 0 1.125.225 1.38.585 3.669.145 4.313 2.686 4.694 5.444.255 1.838.315 2.3.182 1.387l.083.59c.068.448.554.737.982.516.144-.075.254-.231.328-.47a.2.2 0 01.258-.13l.625.22a.2.2 0 01.124.238 2.172 2.172 0 01-.51.92c-.878.917-2.757.664-3.08-.62-.14-.554-.055-.626-.345-1.242-.292-.621-1.238-.709-1.69-.295-.345.315-.407.805-.406 1.282L12.6 15.9a.9.9 0 01-.9.9h-1.4a.9.9 0 01-.9-.9v-.65a1.15 1.15 0 10-2.3 0v.65a.9.9 0 01-.9.9H4.8a.9.9 0 01-.9-.9l.035-3.239c.012-1.884.356-3.658 2.47-4.134.2-.045.252.13.29.342.025.154.043.252.053.294.701 3.058 1.75 4.299 3.144 3.722l.66-.331.254-.13c.158-.082.25-.131.276-.15.012-.01-.165-.206-.407-.464l-1.012-1.067a8.925 8.925 0 01-.199-.216c-.047-.034-.116.068-.208.306-.074.157-.251.252-.272.326-.013.058.108.298.362.72.164.288.22.508-.31.343-1.04-.8-1.518-2.273-1.684-3.725-.004-.035-.162-1.913-.162-1.913a1.2 1.2 0 011.113-1.281L9.9 5.3zm12.994 8.68c.037.697-.403.704-1.213.591l-1.783-.276c-.265-.053-.385-.099-.313-.147.47-.315 3.268-.93 3.31-.168zm-.915-.083l-.926.042c-.85.077-1.452.24.338.336l.103.003c.815.012 1.264-.359.485-.381zm1.667-3.601h.01c.79.398.067 1.03-.65 1.393-.14.07-.491.176-1.052.315-.241.04-.457.092-.333.16l.01.005c1.952.958-3.123 1.534-2.495 1.285l.38-.148c.68-.266 1.614-.682 1.666-1.337.038-.48 1.253-.442 1.493-.968.048-.106 0-.236-.144-.389-.05-.047-.094-.094-.107-.148-.073-.305.7-.431 1.222-.168zm-2.568-.474c-.135 1.198-2.479 4.192-1.949 2.863l.017-.042c.298-.717.376-2.221 1.337-3.221.25-.26.636.035.595.4zm-7.976-.253c.02-.694 1.002-.968 1.346-.347.01-1.274-1.941-.768-1.346.347z"
|
||||
:fill="fill!.fill" fill-rule="evenodd" />
|
||||
</svg>
|
||||
<svg v-else fill="currentColor" fill-rule="evenodd" :height="size" style="flex: none; line-height: 1;"
|
||||
:style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
viewBox="0 0 24 24" :width="size" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<path
|
||||
d="M9.917 2c4.906 0 10.178 3.947 8.93 10.58-.014.07-.037.14-.057.21l-.003-.277c-.083-3-1.534-8.934-8.87-8.934-3.393 0-8.137 3.054-7.93 8.158-.04 4.778 3.555 8.4 7.95 8.332l.073-.001c1.2-.033 2.763-.429 3.1-1.657.063-.031.26.534.268.598.048.256.112.369.192.34.981-.348 2.286-1.222 1.952-2.38-.176-.61-1.775-.147-1.921-.347.418-.979 2.234-.926 3.153-.716.443.102.657.38 1.012.442.29.052.981-.2.96.242C17.226 19.632 13.833 22 9.918 22 3.654 22 0 16.574 0 11.737 0 5.947 4.959 2 9.917 2zM9.9 5.3c.484 0 1.125.225 1.38.585 3.669.145 4.313 2.686 4.694 5.444.255 1.838.315 2.3.182 1.387l.083.59c.068.448.554.737.982.516.144-.075.254-.231.328-.47a.2.2 0 01.258-.13l.625.22a.2.2 0 01.124.238 2.172 2.172 0 01-.51.92c-.878.917-2.757.664-3.08-.62-.14-.554-.055-.626-.345-1.242-.292-.621-1.238-.709-1.69-.295-.345.315-.407.805-.406 1.282L12.6 15.9a.9.9 0 01-.9.9h-1.4a.9.9 0 01-.9-.9v-.65a1.15 1.15 0 10-2.3 0v.65a.9.9 0 01-.9.9H4.8a.9.9 0 01-.9-.9l.035-3.239c.012-1.884.356-3.658 2.47-4.134.2-.045.252.13.29.342.025.154.043.252.053.294.701 3.058 1.75 4.299 3.144 3.722l.66-.331.254-.13c.158-.082.25-.131.276-.15.012-.01-.165-.206-.407-.464l-1.012-1.067a8.925 8.925 0 01-.199-.216c-.047-.034-.116.068-.208.306-.074.157-.251.252-.272.326-.013.058.108.298.362.72.164.288.22.508-.31.343-1.04-.8-1.518-2.273-1.684-3.725-.004-.035-.162-1.913-.162-1.913a1.2 1.2 0 011.113-1.281L9.9 5.3zm12.994 8.68c.037.697-.403.704-1.213.591l-1.783-.276c-.265-.053-.385-.099-.313-.147.47-.315 3.268-.93 3.31-.168zm-.915-.083l-.926.042c-.85.077-1.452.24.338.336l.103.003c.815.012 1.264-.359.485-.381zm1.667-3.601h.01c.79.398.067 1.03-.65 1.393-.14.07-.491.176-1.052.315-.241.04-.457.092-.333.16l.01.005c1.952.958-3.123 1.534-2.495 1.285l.38-.148c.68-.266 1.614-.682 1.666-1.337.038-.48 1.253-.442 1.493-.968.048-.106 0-.236-.144-.389-.05-.047-.094-.094-.107-.148-.073-.305.7-.431 1.222-.168zm-2.568-.474c-.135 1.198-2.479 4.192-1.949 2.863l.017-.042c.298-.717.376-2.221 1.337-3.221.25-.26.636.035.595.4zm-7.976-.253c.02-.694 1.002-.968 1.346-.347.01-1.274-1.941-.768-1.346.347z" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
size?: string | number;
|
||||
color?: boolean;
|
||||
avatar?: boolean;
|
||||
}>();
|
||||
|
||||
const TITLE = 'Claude';
|
||||
|
||||
const BACKGROUND_COLOR = "#D97757";
|
||||
const AVATAR_SCALE = 0.75;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inline-flex items-center justify-center"
|
||||
:style="[`width: ${size}px; height: ${size}px;`, avatar ? `background-color: ${BACKGROUND_COLOR}; border-radius: 0.375rem;` : '']">
|
||||
<svg :style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
:fill="color && !avatar ? BACKGROUND_COLOR : 'currentColor'"
|
||||
:fill-rule="color && !avatar ? 'evenodd' : 'nonzero'" style="flex: none; line-height: 1;"
|
||||
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<path
|
||||
d="M4.709 15.955l4.72-2.647.08-.23-.08-.128H9.2l-.79-.048-2.698-.073-2.339-.097-2.266-.122-.571-.121L0 11.784l.055-.352.48-.321.686.06 1.52.103 2.278.158 1.652.097 2.449.255h.389l.055-.157-.134-.098-.103-.097-2.358-1.596-2.552-1.688-1.336-.972-.724-.491-.364-.462-.158-1.008.656-.722.881.06.225.061.893.686 1.908 1.476 2.491 1.833.365.304.145-.103.019-.073-.164-.274-1.355-2.446-1.446-2.49-.644-1.032-.17-.619a2.97 2.97 0 01-.104-.729L6.283.134 6.696 0l.996.134.42.364.62 1.414 1.002 2.229 1.555 3.03.456.898.243.832.091.255h.158V9.01l.128-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.584.28.48.685-.067.444-.286 1.851-.559 2.903-.364 1.942h.212l.243-.242.985-1.306 1.652-2.064.73-.82.85-.904.547-.431h1.033l.76 1.129-.34 1.166-1.064 1.347-.881 1.142-1.264 1.7-.79 1.36.073.11.188-.02 2.856-.606 1.543-.28 1.841-.315.833.388.091.395-.328.807-1.969.486-2.309.462-3.439.813-.042.03.049.061 1.549.146.662.036h1.622l3.02.225.79.522.474.638-.079.485-1.215.62-1.64-.389-3.829-.91-1.312-.329h-.182v.11l1.093 1.068 2.006 1.81 2.509 2.33.127.578-.322.455-.34-.049-2.205-1.657-.851-.747-1.926-1.62h-.128v.17l.444.649 2.345 3.521.122 1.08-.17.353-.608.213-.668-.122-1.374-1.925-1.415-2.167-1.143-1.943-.14.08-.674 7.254-.316.37-.729.28-.607-.461-.322-.747.322-1.476.389-1.924.315-1.53.286-1.9.17-.632-.012-.042-.14.018-1.434 1.967-2.18 2.945-1.726 1.845-.414.164-.717-.37.067-.662.401-.589 2.388-3.036 1.44-1.882.93-1.086-.006-.158h-.055L4.132 18.56l-1.13.146-.487-.456.061-.746.231-.243 1.908-1.312-.006.006z" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
size?: string | number;
|
||||
color?: boolean;
|
||||
avatar?: boolean;
|
||||
}>();
|
||||
|
||||
const TITLE = 'Cohere';
|
||||
|
||||
const BACKGROUND_COLOR = "#151617";
|
||||
const AVATAR_SCALE = 0.6;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inline-flex items-center justify-center"
|
||||
:style="[`width: ${size}px; height: ${size}px;`, avatar ? `background-color: ${BACKGROUND_COLOR}; border-radius: 0.375rem;` : '']">
|
||||
<svg :fill="!avatar ? 'currentColor' : ''" fill-rule="evenodd" :height="size"
|
||||
style="flex: none; line-height: 1;"
|
||||
:style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
viewBox="0 0 24 24" :width="size" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<path clip-rule="evenodd" :fill="color ? '#39594D' : ''"
|
||||
d="M8.128 14.099c.592 0 1.77-.033 3.398-.703 1.897-.781 5.672-2.2 8.395-3.656 1.905-1.018 2.74-2.366 2.74-4.18A4.56 4.56 0 0018.1 1H7.549A6.55 6.55 0 001 7.55c0 3.617 2.745 6.549 7.128 6.549z" />
|
||||
<path clip-rule="evenodd" :fill="color ? '#D18EE2' : ''"
|
||||
d="M9.912 18.61a4.387 4.387 0 012.705-4.052l3.323-1.38c3.361-1.394 7.06 1.076 7.06 4.715a5.104 5.104 0 01-5.105 5.104l-3.597-.001a4.386 4.386 0 01-4.386-4.387z" />
|
||||
<path :fill="color ? '#FF7759' : ''"
|
||||
d="M4.776 14.962A3.775 3.775 0 001 18.738v.489a3.776 3.776 0 007.551 0v-.49a3.775 3.775 0 00-3.775-3.775z" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
size?: string | number;
|
||||
color?: boolean;
|
||||
avatar?: boolean;
|
||||
}>();
|
||||
|
||||
const TITLE = 'Deep Cogito';
|
||||
|
||||
const BACKGROUND_COLOR = "#4e81ee";
|
||||
const AVATAR_SCALE = 0.7;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inline-flex items-center justify-center"
|
||||
:style="[`width: ${size}px; height: ${size}px;`, avatar ? `background-color: ${BACKGROUND_COLOR}; border-radius: 0.375rem;` : '']">
|
||||
<svg :style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
:fill="color && !avatar ? BACKGROUND_COLOR : 'currentColor'"
|
||||
:fill-rule="color && !avatar ? 'evenodd' : 'nonzero'" style="flex: none; line-height: 1;"
|
||||
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<path
|
||||
d="M19.74 21.618l4.213-10.534a.412.412 0 00.027-.1l.003-.03a.404.404 0 00-.02-.167l-.007-.026a.44.44 0 00-.045-.085l-.003-.005L16.528.13c-.002-.003 0-.006-.003-.01h.004v.173h-.01c-.102-.254-.277-.285-.445-.248l-.001-.003c-.001.001-.003-.003-.003-.003h-.006s0-.038-.002-.038L4.466 3.143c-.006.002-.012-.005-.018-.003-.022.007-.044.01-.064.021a.42.42 0 00-.039.022c-.013.008-.026.015-.037.025a.464.464 0 00-.118.154l-.01.017L0 13.919c-.004.013.023.026.023.04v.148c0 .019 0 .038.003.057l-.003.001v.006c0 .049-.011.096.016.139H.03c0 .002-.008.002-.008.002l-.01.001c.007.012.008.023.016.034l7.377 9.486v.005a.063.063 0 00.006.005c.006.008.013.013.02.02a.258.258 0 00.02.02c.008.008.015.017.023.023l.02.014a.564.564 0 00.104.053c.011.003.021.008.032.01.005.002.01.005.016.006a.445.445 0 00.172.004l11.597-2.108a.362.362 0 00.041-.01l.011.001h.001c.005 0 .01-.007.015-.009.01-.003.02-.008.03-.013a.248.248 0 00.025-.012l.01-.007a.448.448 0 00.096-.072c.004-.004.006-.01.01-.013a.419.419 0 00.069-.1l.005-.008.003-.006.001-.003.006-.015zm-.77-.894l-7.315-4.573 8.229-6.4-.915 10.973zm-7.82-5.27l-2.782-10.2 11.127 3.71-8.345 6.49zm9.611-5.883l2.267 1.512-3.022 7.554.755-9.066zm1.133-.278l-1.168-.778-1.556-3.113 2.724 3.891zm-2.35-1.22L8.862 4.514l7.122-3.56 3.56 7.12zM7.71 4.129L6.063 3.58 12.1 1.932 7.71 4.128zm-2.881-.053l2.201.734-5.137 6.605 2.936-7.34zm2.725 1.46l2.754 10.096-9.179-1.835 6.425-8.26zM10.3 16.508L7.633 22.73l-6.223-8 8.89 1.777zm.803.313l7.065 4.417-9.716 1.767 2.65-6.184z" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
size?: string | number;
|
||||
color?: boolean;
|
||||
avatar?: boolean;
|
||||
}>();
|
||||
|
||||
const TITLE = 'DeepMind';
|
||||
|
||||
const BACKGROUND_COLOR = "#4285F4";
|
||||
const AVATAR_SCALE = 0.75;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inline-flex items-center justify-center"
|
||||
:style="[`width: ${size}px; height: ${size}px;`, avatar ? `background-color: ${BACKGROUND_COLOR}; border-radius: 0.375rem;` : '']">
|
||||
<svg :style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
:fill="color && !avatar ? BACKGROUND_COLOR : 'currentColor'"
|
||||
:fill-rule="color && !avatar ? 'evenodd' : 'nonzero'" style="flex: none; line-height: 1;"
|
||||
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<path
|
||||
d="M5.988 1.622A8.539 8.539 0 003.45 8.446c.349 4.408 4.506 7.995 8.276 7.995 3.507 0 4.88-3.061 4.541-5.14a4.318 4.318 0 00-.95-2.073c.632.34 1.244.776 1.809 1.3 1.52 1.415 2.44 3.229 2.587 5.1C20.04 19.763 16.98 24 11.863 24c-1.695 0-3.48-.432-4.98-1.143C2.816 20.937 0 16.797 0 12.002 0 7.571 2.405 3.7 5.988 1.622zM12.136 0c1.696 0 3.481.432 4.98 1.143C21.186 3.063 24 7.203 24 11.998c0 4.431-2.405 8.303-5.988 10.38a8.539 8.539 0 002.538-6.824c-.349-4.408-4.506-7.995-8.276-7.995-3.507 0-4.88 3.061-4.541 5.14a4.3 4.3 0 00.953 2.073 8.723 8.723 0 01-1.81-1.3c-1.52-1.415-2.44-3.227-2.589-5.1C3.96 4.237 7.02 0 12.137 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
size?: string | number;
|
||||
color?: boolean;
|
||||
avatar?: boolean;
|
||||
}>();
|
||||
|
||||
const TITLE = 'DeepSeek';
|
||||
|
||||
const BACKGROUND_COLOR = "#4D6BFE";
|
||||
const AVATAR_SCALE = 0.75;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inline-flex items-center justify-center"
|
||||
:style="[`width: ${size}px; height: ${size}px;`, avatar ? `background-color: ${BACKGROUND_COLOR}; border-radius: 0.375rem;` : '']">
|
||||
<svg :style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
:fill="color && !avatar ? BACKGROUND_COLOR : 'currentColor'"
|
||||
:fill-rule="color && !avatar ? 'evenodd' : 'nonzero'" style="flex: none; line-height: 1;"
|
||||
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<path
|
||||
d="M23.748 4.482c-.254-.124-.364.113-.512.234-.051.039-.094.09-.137.136-.372.397-.806.657-1.373.626-.829-.046-1.537.214-2.163.848-.133-.782-.575-1.248-1.247-1.548-.352-.156-.708-.311-.955-.65-.172-.241-.219-.51-.305-.774-.055-.16-.11-.323-.293-.35-.2-.031-.278.136-.356.276-.313.572-.434 1.202-.422 1.84.027 1.436.633 2.58 1.838 3.393.137.093.172.187.129.323-.082.28-.18.552-.266.833-.055.179-.137.217-.329.14a5.526 5.526 0 01-1.736-1.18c-.857-.828-1.631-1.742-2.597-2.458a11.365 11.365 0 00-.689-.471c-.985-.957.13-1.743.388-1.836.27-.098.093-.432-.779-.428-.872.004-1.67.295-2.687.684a3.055 3.055 0 01-.465.137 9.597 9.597 0 00-2.883-.102c-1.885.21-3.39 1.102-4.497 2.623C.082 8.606-.231 10.684.152 12.85c.403 2.284 1.569 4.175 3.36 5.653 1.858 1.533 3.997 2.284 6.438 2.14 1.482-.085 3.133-.284 4.994-1.86.47.234.962.327 1.78.397.63.059 1.236-.03 1.705-.128.735-.156.684-.837.419-.961-2.155-1.004-1.682-.595-2.113-.926 1.096-1.296 2.746-2.642 3.392-7.003.05-.347.007-.565 0-.845-.004-.17.035-.237.23-.256a4.173 4.173 0 001.545-.475c1.396-.763 1.96-2.015 2.093-3.517.02-.23-.004-.467-.247-.588zM11.581 18c-2.089-1.642-3.102-2.183-3.52-2.16-.392.024-.321.471-.235.763.09.288.207.486.371.739.114.167.192.416-.113.603-.673.416-1.842-.14-1.897-.167-1.361-.802-2.5-1.86-3.301-3.307-.774-1.393-1.224-2.887-1.298-4.482-.02-.386.093-.522.477-.592a4.696 4.696 0 011.529-.039c2.132.312 3.946 1.265 5.468 2.774.868.86 1.525 1.887 2.202 2.891.72 1.066 1.494 2.082 2.48 2.914.348.292.625.514.891.677-.802.09-2.14.11-3.054-.614zm1-6.44a.306.306 0 01.415-.287.302.302 0 01.2.288.306.306 0 01-.31.307.303.303 0 01-.304-.308zm3.11 1.596c-.2.081-.399.151-.59.16a1.245 1.245 0 01-.798-.254c-.274-.23-.47-.358-.552-.758a1.73 1.73 0 01.016-.588c.07-.327-.008-.537-.239-.727-.187-.156-.426-.199-.688-.199a.559.559 0 01-.254-.078c-.11-.054-.2-.19-.114-.358.028-.054.16-.186.192-.21.356-.202.767-.136 1.146.016.352.144.618.408 1.001.782.391.451.462.576.685.914.176.265.336.537.445.848.067.195-.019.354-.25.452z" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,42 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
size?: string | number;
|
||||
color?: boolean;
|
||||
avatar?: boolean;
|
||||
}>();
|
||||
|
||||
const TITLE = 'Essential AI';
|
||||
|
||||
const BACKGROUND_COLOR = "linear-gradient(135deg, #5E38A5, #31018C 63%)";
|
||||
const AVATAR_SCALE = 0.7;
|
||||
|
||||
const [fill] = useFillIds(TITLE, 1);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inline-flex items-center justify-center"
|
||||
:style="[`width: ${size}px; height: ${size}px;`, avatar ? `background: ${BACKGROUND_COLOR}; border-radius: 0.375rem;` : '']">
|
||||
<svg v-if="color && !avatar"
|
||||
:style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<path
|
||||
d="M3.429 10.75c0 2.32.903 4.546 2.51 6.187A8.484 8.484 0 0012 19.5a8.484 8.484 0 006.06-2.563 8.843 8.843 0 002.511-6.187H24c0 3.249-1.264 6.365-3.515 8.662A11.877 11.877 0 0112 23c-3.183 0-6.235-1.29-8.485-3.588A12.38 12.38 0 010 10.75h3.429zm13.714 0a5.306 5.306 0 01-1.507 3.712A5.09 5.09 0 0112 16a5.09 5.09 0 01-3.637-1.538 5.306 5.306 0 01-1.506-3.712h10.286zM12 2c2.273 0 4.453.922 6.06 2.563a8.843 8.843 0 012.511 6.187h-3.428a5.306 5.306 0 00-1.507-3.712A5.09 5.09 0 0012 5.5a5.09 5.09 0 00-3.637 1.538 5.306 5.306 0 00-1.506 3.712H3.43c0-2.32.903-4.546 2.51-6.187A8.484 8.484 0 0112 2z"
|
||||
:fill="fill!.fill" />
|
||||
<defs>
|
||||
<radialGradient cx="0" cy="0" gradientTransform="matrix(21.6991 27.0254 -47.9838 40.1491 2.3 -4.025)"
|
||||
gradientUnits="userSpaceOnUse" :id="fill!.id" r="1">
|
||||
<stop stop-color="#6A46AC" />
|
||||
<stop offset=".63" stop-color="#31008C" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
<svg v-else fill="currentColor" fill-rule="evenodd" :height="size" style="flex: none; line-height: 1;"
|
||||
:style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
viewBox="0 0 24 24" :width="size" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<path
|
||||
d="M3.429 10.75c0 2.32.903 4.546 2.51 6.187A8.484 8.484 0 0012 19.5a8.484 8.484 0 006.06-2.563 8.843 8.843 0 002.511-6.187H24c0 3.249-1.264 6.365-3.515 8.662A11.877 11.877 0 0112 23c-3.183 0-6.235-1.29-8.485-3.588A12.38 12.38 0 010 10.75h3.429zm13.714 0a5.306 5.306 0 01-1.507 3.712A5.09 5.09 0 0112 16a5.09 5.09 0 01-3.637-1.538 5.306 5.306 0 01-1.506-3.712h10.286zM12 2c2.273 0 4.453.922 6.06 2.563a8.843 8.843 0 012.511 6.187h-3.428a5.306 5.306 0 00-1.507-3.712A5.09 5.09 0 0012 5.5a5.09 5.09 0 00-3.637 1.538 5.306 5.306 0 00-1.506 3.712H3.43c0-2.32.903-4.546 2.51-6.187A8.484 8.484 0 0112 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,54 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
size?: string | number;
|
||||
color?: boolean;
|
||||
avatar?: boolean;
|
||||
}>();
|
||||
|
||||
const TITLE = 'GLM-V';
|
||||
|
||||
const BACKGROUND_COLOR = "#0039C6";
|
||||
const AVATAR_SCALE = 0.7;
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inline-flex items-center justify-center"
|
||||
:style="[`width: ${size}px; height: ${size}px;`, avatar ? `background-color: ${BACKGROUND_COLOR}; border-radius: 0.375rem;` : '']">
|
||||
<svg v-if="color && !avatar"
|
||||
:style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<path
|
||||
d="M19.028 3.853a1.31 1.31 0 101.309 1.309 1.31 1.31 0 00-1.31-1.31zm.594 1.987a.664.664 0 11.001-1.329.664.664 0 01-.001 1.329z"
|
||||
fill="#0039C6" />
|
||||
<path
|
||||
d="M19.67 4.814a.348.348 0 000 .695.348.348 0 000-.695zM14.117 16.317a.037.037 0 00-.027-.061c-.314-.005-1.499-.093-2.105-1.082a2.088 2.088 0 01-.089-2.027.04.04 0 000-.033l-1.005-1.81c-.008-.016-.027-.02-.044-.017-2.018.396-3.194 2.104-2.838 3.788.096.458.285.864.578 1.237.748.95 1.859 1.26 2.675 1.235 1.69-.05 2.63-.977 2.856-1.23zM4.766 6.906a9.903 9.903 0 012.487-2.153L5.63 1.829a.58.58 0 00-.514-.298l-4.809.04a.31.31 0 00-.268.46l3.62 6.525c.41-.756.823-1.311 1.106-1.65zM17.52 10.93c-.422.01-.824-.01-1.044.027-.183.031-.657.121-1.14.574a.037.037 0 00.016.062c.329.087.633.237.928.416.63.383 1.205 1.346 1.383 1.8.01.03.051.032.066.004l1.72-3.27c.015-.03-.015-.064-.047-.051-.806.328-1.476.43-1.883.439h.001zM23.687 1.512l-4.546.037a.58.58 0 00-.507.306l-.126.27c.364-.009 2.426.152 4.158 2.308l1.298-2.467a.31.31 0 00-.277-.455v.001zM10.846 21.497l.843 1.517a.31.31 0 00.533.016l1.31-2.064a8.27 8.27 0 01-2.686.53z"
|
||||
fill="#0039C6" />
|
||||
<path
|
||||
d="M22.327 5.379c-1.461-2.435-3.529-2.393-3.529-2.393-.951-.186-1.436-1.263-3.626-1.797-2.046-.5-3.188.112-2.368.451 1.322.548 2.075 2.73 2.462 4.374.494 2.1 1.569 2.14 2.758 2.011.924-.098 2.775-1.058 3.721-1.256 1.03-.216 1.353-.101.58-1.39h.002zm-3.373 1.538a1.757 1.757 0 11.001-3.514 1.757 1.757 0 010 3.514zM12.756 13.364c-.261.623.032 1.57 1.316 1.656.08 0 .12-.053.12-.12 0-.065-.058-.097-.12-.119-.062-.022-.654-.226-.524-.918.042-.23.248-.438.5-.533-.142-.265-.143-.622.06-.965-.343 0-1.015.194-1.353 1v-.001zM16.836 14.624c0-.684-.451-1.41-.805-1.64-.307-.107-.816.06-1.103.487.337.22.553.572.59 1.48.046 1.174-.844 2.184-2.035 2.76 1.764-.34 3.353-1.79 3.353-3.087zM6.201 16.591C2.802 12.248 4.841 6.44 9.795 4.472c-6.168 2.08-8.01 8.89-4.547 13.168 2.299 2.84 6.254 2.937 8.373 2.2-2.571-.213-5.33-.578-7.42-3.248zM15.09 10.445c.03-.012.03-.054 0-.067-.06-.028-.175-.05-.387.004-.411.102-1.42.264-1.34-.226.055-.349.614-.46 1.023-1.128.202-.33-.06-.875-.59-.875-.408 0-.828.186-1.285.67-.64.676-.766 1.31-.518 1.776.234.442.784.633 1.702.48.628-.103.82-.381 1.396-.635v.001z"
|
||||
fill="#0039C6" />
|
||||
<path
|
||||
d="M14.685 12.364s-.492.177-.436.934c.183-.046.239-.028.488.092.114-.338.407-.623.84-.623.078 0 .155.009.23.024-.29-.219-.591-.4-1.12-.427h-.002zM18.253 9.573c-2.444.515-2.48-1.58-4.28-1.985-2.423-.544-5.526.905-6.888 3.545-.554 1.074-1.076 3.178.283 4.97-.854-2.323.434-4.032 1.264-4.702.782-.629 1.738-1.026 2.682-1.267a.035.035 0 00.027-.034c.016-.51.276-1.055.767-1.574.521-.552 1.055-.819 1.631-.819.422 0 .793.215.994.573.185.33.185.725 0 1.026-.11.18-.228.326-.345.447-.022.021-.006.06.024.06 1.132.071 2.063.347 2.893.284 3.134-.24 4.25-1.91 4.652-2.217.365-.278-2.385 1.413-3.706 1.69l.002.003z"
|
||||
fill="#0039C6" />
|
||||
<path
|
||||
d="M15.159 16.452c-.916.916-2.416 2.208-5.318 1.665-2.105-.394-3.719-3.312-3.25-6.832.252-1.883 1.712-3.64 3.082-4.206 2.995-1.24 4.57.081 5.298.897.516.579 1.106 1.773 3.014 1.757 1.456-.012 3.978-1.726 4.678-2.372.24-.221-.021-.343-.418-.308-1.514.136-2.916 1.752-4.976 1.664-1.017-.043-2.298-.742-2.75-2.25-.542-1.812-.553-2.19-1.176-2.297-.653-.112-1.648-.173-3.087.275-1.863.583-4.784 3.436-4.97 6.84-.324 5.884 2.306 8.243 6.36 8.743.459.056 1.462-.066 1.909-.162a5.727 5.727 0 001.565-.967c1.01-.908 1.626-1.918 1.77-3.376.05-.49-.069-1.14-.135-1.425-.023.302-.623 1.376-1.597 2.35v.004z"
|
||||
fill="#0039C6" />
|
||||
</svg>
|
||||
<svg v-else fill="currentColor" fill-rule="evenodd" :height="size" style="flex: none; line-height: 1;"
|
||||
:style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
viewBox=" 0 0 24 24" :width="size" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<path
|
||||
d="M19.028 3.853a1.31 1.31 0 101.309 1.309 1.31 1.31 0 00-1.31-1.31zm.594 1.987a.664.664 0 11.001-1.329.664.664 0 01-.001 1.329z" />
|
||||
<path
|
||||
d="M19.67 4.814a.348.348 0 000 .695.348.348 0 000-.695zM14.117 16.317a.037.037 0 00-.027-.061c-.314-.005-1.499-.093-2.105-1.082a2.088 2.088 0 01-.089-2.027.04.04 0 000-.033l-1.005-1.81c-.008-.016-.027-.02-.044-.017-2.018.396-3.194 2.104-2.838 3.788.096.458.285.864.578 1.237.748.95 1.859 1.26 2.675 1.235 1.69-.05 2.63-.977 2.856-1.23zM4.766 6.906a9.903 9.903 0 012.487-2.153L5.63 1.829a.58.58 0 00-.514-.298l-4.809.04a.31.31 0 00-.268.46l3.62 6.525c.41-.756.823-1.311 1.106-1.65zM17.52 10.93c-.422.01-.824-.01-1.044.027-.183.031-.657.121-1.14.574a.037.037 0 00.016.062c.329.087.633.237.928.416.63.383 1.205 1.346 1.383 1.8.01.03.051.032.066.004l1.72-3.27c.015-.03-.015-.064-.047-.051-.806.328-1.476.43-1.883.439h.001zM23.687 1.512l-4.546.037a.58.58 0 00-.507.306l-.126.27c.364-.009 2.426.152 4.158 2.308l1.298-2.467a.31.31 0 00-.277-.455v.001zM10.846 21.497l.843 1.517a.31.31 0 00.533.016l1.31-2.064a8.27 8.27 0 01-2.686.53z" />
|
||||
<path
|
||||
d="M22.327 5.379c-1.461-2.435-3.529-2.393-3.529-2.393-.951-.186-1.436-1.263-3.626-1.797-2.046-.5-3.188.112-2.368.451 1.322.548 2.075 2.73 2.462 4.374.494 2.1 1.569 2.14 2.758 2.011.924-.098 2.775-1.058 3.721-1.256 1.03-.216 1.353-.101.58-1.39h.002zm-3.373 1.538a1.757 1.757 0 11.001-3.514 1.757 1.757 0 010 3.514zM12.756 13.364c-.261.623.032 1.57 1.316 1.656.08 0 .12-.053.12-.12 0-.065-.058-.097-.12-.119-.062-.022-.654-.226-.524-.918.042-.23.248-.438.5-.533-.142-.265-.143-.622.06-.965-.343 0-1.015.194-1.353 1v-.001zM16.836 14.624c0-.684-.451-1.41-.805-1.64-.307-.107-.816.06-1.103.487.337.22.553.572.59 1.48.046 1.174-.844 2.184-2.035 2.76 1.764-.34 3.353-1.79 3.353-3.087zM6.201 16.591C2.802 12.248 4.841 6.44 9.795 4.472c-6.168 2.08-8.01 8.89-4.547 13.168 2.299 2.84 6.254 2.937 8.373 2.2-2.571-.213-5.33-.578-7.42-3.248zM15.09 10.445c.03-.012.03-.054 0-.067-.06-.028-.175-.05-.387.004-.411.102-1.42.264-1.34-.226.055-.349.614-.46 1.023-1.128.202-.33-.06-.875-.59-.875-.408 0-.828.186-1.285.67-.64.676-.766 1.31-.518 1.776.234.442.784.633 1.702.48.628-.103.82-.381 1.396-.635v.001z" />
|
||||
<path
|
||||
d="M14.685 12.364s-.492.177-.436.934c.183-.046.239-.028.488.092.114-.338.407-.623.84-.623.078 0 .155.009.23.024-.29-.219-.591-.4-1.12-.427h-.002zM18.253 9.573c-2.444.515-2.48-1.58-4.28-1.985-2.423-.544-5.526.905-6.888 3.545-.554 1.074-1.076 3.178.283 4.97-.854-2.323.434-4.032 1.264-4.702.782-.629 1.738-1.026 2.682-1.267a.035.035 0 00.027-.034c.016-.51.276-1.055.767-1.574.521-.552 1.055-.819 1.631-.819.422 0 .793.215.994.573.185.33.185.725 0 1.026-.11.18-.228.326-.345.447-.022.021-.006.06.024.06 1.132.071 2.063.347 2.893.284 3.134-.24 4.25-1.91 4.652-2.217.365-.278-2.385 1.413-3.706 1.69l.002.003z" />
|
||||
<path
|
||||
d="M15.159 16.452c-.916.916-2.416 2.208-5.318 1.665-2.105-.394-3.719-3.312-3.25-6.832.252-1.883 1.712-3.64 3.082-4.206 2.995-1.24 4.57.081 5.298.897.516.579 1.106 1.773 3.014 1.757 1.456-.012 3.978-1.726 4.678-2.372.24-.221-.021-.343-.418-.308-1.514.136-2.916 1.752-4.976 1.664-1.017-.043-2.298-.742-2.75-2.25-.542-1.812-.553-2.19-1.176-2.297-.653-.112-1.648-.173-3.087.275-1.863.583-4.784 3.436-4.97 6.84-.324 5.884 2.306 8.243 6.36 8.743.459.056 1.462-.066 1.909-.162a5.727 5.727 0 001.565-.967c1.01-.908 1.626-1.918 1.77-3.376.05-.49-.069-1.14-.135-1.425-.023.302-.623 1.376-1.597 2.35v.004z" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
@@ -12,15 +12,17 @@ const TITLE = 'Gemini';
|
||||
const [a, b, c] = useFillIds(TITLE, 3);
|
||||
|
||||
const BACKGROUND_COLOR = "#fff";
|
||||
const AVATAR_SCALE = 0.8;
|
||||
|
||||
const d = "M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:style="[`max-width: ${size}px; max-height: ${size}px;`, avatar ? `background-color: ${BACKGROUND_COLOR}; border-radius: 9999px; padding: 0.25rem;` : '']">
|
||||
<svg v-if="color" class="w-full h-full" style="flex: none; line-height: 1;" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<div class="inline-flex items-center justify-center"
|
||||
:style="[`width: ${size}px; height: ${size}px;`, avatar ? `background-color: ${BACKGROUND_COLOR}; border-radius: 0.375rem;` : '']">
|
||||
<svg v-if="color"
|
||||
:style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
|
||||
<!-- Base Layer -->
|
||||
@@ -48,11 +50,11 @@ const d = "M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
<svg v-else fill="currentColor" fillRule="evenodd" :height="size" style="flex: none; line-height: 1;"
|
||||
<svg v-else fill="currentColor" fill-rule="evenodd" :height="size" style="flex: none; line-height: 1;"
|
||||
:style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
viewBox="0 0 24 24" :width="size" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<path
|
||||
d="M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z" />
|
||||
<path :d="d" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
size?: string | number;
|
||||
color?: boolean;
|
||||
avatar?: boolean;
|
||||
}>();
|
||||
|
||||
const TITLE = 'Gemma';
|
||||
|
||||
const BACKGROUND_COLOR = "linear-gradient(45deg, #446EFF 14%, #2E96FF 40%, #B1C5FF 73%)";
|
||||
const AVATAR_SCALE = 1;
|
||||
|
||||
const [fill] = useFillIds(TITLE, 1);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inline-flex items-center justify-center"
|
||||
:style="[`width: ${size}px; height: ${size}px;`, avatar ? `background: ${BACKGROUND_COLOR}; border-radius: 0.375rem;` : '']">
|
||||
<svg v-if="color && !avatar"
|
||||
:style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<defs>
|
||||
<linearGradient :id="fill!.id" x1="24.419%" x2="75.194%" y1="75.581%" y2="25.194%">
|
||||
<stop offset="0%" stopColor="#446EFF" />
|
||||
<stop offset="36.661%" stopColor="#2E96FF" />
|
||||
<stop offset="83.221%" stopColor="#B1C5FF" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path
|
||||
d="M12.34 5.953a8.233 8.233 0 01-.247-1.125V3.72a8.25 8.25 0 015.562 2.232H12.34zm-.69 0c.113-.373.199-.755.257-1.145V3.72a8.25 8.25 0 00-5.562 2.232h5.304zm-5.433.187h5.373a7.98 7.98 0 01-.267.696 8.41 8.41 0 01-1.76 2.65L6.216 6.14zm-.264-.187H2.977v.187h2.915a8.436 8.436 0 00-2.357 5.767H0v.186h3.535a8.436 8.436 0 002.357 5.767H2.977v.186h2.976v2.977h.187v-2.915a8.436 8.436 0 005.767 2.357V24h.186v-3.535a8.436 8.436 0 005.767-2.357v2.915h.186v-2.977h2.977v-.186h-2.915a8.436 8.436 0 002.357-5.767H24v-.186h-3.535a8.436 8.436 0 00-2.357-5.767h2.915v-.187h-2.977V2.977h-.186v2.915a8.436 8.436 0 00-5.767-2.357V0h-.186v3.535A8.436 8.436 0 006.14 5.892V2.977h-.187v2.976zm6.14 14.326a8.25 8.25 0 005.562-2.233H12.34c-.108.367-.19.743-.247 1.126v1.107zm-.186-1.087a8.015 8.015 0 00-.258-1.146H6.345a8.25 8.25 0 005.562 2.233v-1.087zm-8.186-7.285h1.107a8.23 8.23 0 001.125-.247V6.345a8.25 8.25 0 00-2.232 5.562zm1.087.186H3.72a8.25 8.25 0 002.232 5.562v-5.304a8.012 8.012 0 00-1.145-.258zm15.47-.186a8.25 8.25 0 00-2.232-5.562v5.315c.367.108.743.19 1.126.247h1.107zm-1.086.186c-.39.058-.772.144-1.146.258v5.304a8.25 8.25 0 002.233-5.562h-1.087zm-1.332 5.69V12.41a7.97 7.97 0 00-.696.267 8.409 8.409 0 00-2.65 1.76l3.346 3.346zm0-6.18v-5.45l-.012-.013h-5.451c.076.235.162.468.26.696a8.698 8.698 0 001.819 2.688 8.698 8.698 0 002.688 1.82c.228.097.46.183.696.259zM6.14 17.848V12.41c.235.078.468.167.696.267a8.403 8.403 0 012.688 1.799 8.404 8.404 0 011.799 2.688c.1.228.19.46.267.696H6.152l-.012-.012zm0-6.245V6.326l3.29 3.29a8.716 8.716 0 01-2.594 1.728 8.14 8.14 0 01-.696.259zm6.257 6.257h5.277l-3.29-3.29a8.716 8.716 0 00-1.728 2.594 8.135 8.135 0 00-.259.696zm-2.347-7.81a9.435 9.435 0 01-2.88 1.96 9.14 9.14 0 012.88 1.94 9.14 9.14 0 011.94 2.88 9.435 9.435 0 011.96-2.88 9.14 9.14 0 012.88-1.94 9.435 9.435 0 01-2.88-1.96 9.434 9.434 0 01-1.96-2.88 9.14 9.14 0 01-1.94 2.88z"
|
||||
:fill="fill!.fill" fill-rule="evenodd" />
|
||||
</svg>
|
||||
<svg v-else fill="currentColor" fill-rule="evenodd" :height="size" style="flex: none; line-height: 1;"
|
||||
:style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
viewBox="0 0 24 24" :width="size" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<path
|
||||
d="M12.34 5.953a8.233 8.233 0 01-.247-1.125V3.72a8.25 8.25 0 015.562 2.232H12.34zm-.69 0c.113-.373.199-.755.257-1.145V3.72a8.25 8.25 0 00-5.562 2.232h5.304zm-5.433.187h5.373a7.98 7.98 0 01-.267.696 8.41 8.41 0 01-1.76 2.65L6.216 6.14zm-.264-.187H2.977v.187h2.915a8.436 8.436 0 00-2.357 5.767H0v.186h3.535a8.436 8.436 0 002.357 5.767H2.977v.186h2.976v2.977h.187v-2.915a8.436 8.436 0 005.767 2.357V24h.186v-3.535a8.436 8.436 0 005.767-2.357v2.915h.186v-2.977h2.977v-.186h-2.915a8.436 8.436 0 002.357-5.767H24v-.186h-3.535a8.436 8.436 0 00-2.357-5.767h2.915v-.187h-2.977V2.977h-.186v2.915a8.436 8.436 0 00-5.767-2.357V0h-.186v3.535A8.436 8.436 0 006.14 5.892V2.977h-.187v2.976zm6.14 14.326a8.25 8.25 0 005.562-2.233H12.34c-.108.367-.19.743-.247 1.126v1.107zm-.186-1.087a8.015 8.015 0 00-.258-1.146H6.345a8.25 8.25 0 005.562 2.233v-1.087zm-8.186-7.285h1.107a8.23 8.23 0 001.125-.247V6.345a8.25 8.25 0 00-2.232 5.562zm1.087.186H3.72a8.25 8.25 0 002.232 5.562v-5.304a8.012 8.012 0 00-1.145-.258zm15.47-.186a8.25 8.25 0 00-2.232-5.562v5.315c.367.108.743.19 1.126.247h1.107zm-1.086.186c-.39.058-.772.144-1.146.258v5.304a8.25 8.25 0 002.233-5.562h-1.087zm-1.332 5.69V12.41a7.97 7.97 0 00-.696.267 8.409 8.409 0 00-2.65 1.76l3.346 3.346zm0-6.18v-5.45l-.012-.013h-5.451c.076.235.162.468.26.696a8.698 8.698 0 001.819 2.688 8.698 8.698 0 002.688 1.82c.228.097.46.183.696.259zM6.14 17.848V12.41c.235.078.468.167.696.267a8.403 8.403 0 012.688 1.799 8.404 8.404 0 011.799 2.688c.1.228.19.46.267.696H6.152l-.012-.012zm0-6.245V6.326l3.29 3.29a8.716 8.716 0 01-2.594 1.728 8.14 8.14 0 01-.696.259zm6.257 6.257h5.277l-3.29-3.29a8.716 8.716 0 00-1.728 2.594 8.135 8.135 0 00-.259.696zm-2.347-7.81a9.435 9.435 0 01-2.88 1.96 9.14 9.14 0 012.88 1.94 9.14 9.14 0 011.94 2.88 9.435 9.435 0 011.96-2.88 9.14 9.14 0 012.88-1.94 9.435 9.435 0 01-2.88-1.96 9.434 9.434 0 01-1.96-2.88 9.14 9.14 0 01-1.94 2.88z" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
size?: string | number;
|
||||
color?: boolean;
|
||||
avatar?: boolean;
|
||||
}>();
|
||||
|
||||
const TITLE = 'Google';
|
||||
|
||||
const BACKGROUND_COLOR = "#fff";
|
||||
const AVATAR_SCALE = 0.75;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inline-flex items-center justify-center"
|
||||
:style="[`width: ${size}px; height: ${size}px;`, avatar ? `background-color: ${BACKGROUND_COLOR}; border-radius: 0.375rem;` : '']">
|
||||
<svg :fill="avatar ? 'currentColor' : ''" fill-rule="evenodd" :height="size" style="flex: none; line-height: 1;"
|
||||
:style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
viewBox="0 0 24 24" :width="size" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<path :fill="color ? '#4285F4' : ''"
|
||||
d="M23 12.245c0-.905-.075-1.565-.236-2.25h-10.54v4.083h6.186c-.124 1.014-.797 2.542-2.294 3.569l-.021.136 3.332 2.53.23.022C21.779 18.417 23 15.593 23 12.245z" />
|
||||
<path :fill="color ? '#34A853' : ''"
|
||||
d="M12.225 23c3.03 0 5.574-.978 7.433-2.665l-3.542-2.688c-.948.648-2.22 1.1-3.891 1.1a6.745 6.745 0 01-6.386-4.572l-.132.011-3.465 2.628-.045.124C4.043 20.531 7.835 23 12.225 23z" />
|
||||
<path :fill="color ? '#FBBC05' : ''"
|
||||
d="M5.84 14.175A6.65 6.65 0 015.463 12c0-.758.138-1.491.361-2.175l-.006-.147-3.508-2.67-.115.054A10.831 10.831 0 001 12c0 1.772.436 3.447 1.197 4.938l3.642-2.763z" />
|
||||
<path :fill="color ? '#EB4335' : ''"
|
||||
d="M12.225 5.253c2.108 0 3.529.892 4.34 1.638l3.167-3.031C17.787 2.088 15.255 1 12.225 1 7.834 1 4.043 3.469 2.197 7.062l3.63 2.763a6.77 6.77 0 016.398-4.572z" />
|
||||
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
@@ -6,15 +6,17 @@ defineProps<{
|
||||
}>();
|
||||
|
||||
const TITLE = 'Grok';
|
||||
const AVATAR_SCALE = 0.7;
|
||||
|
||||
const BACKGROUND_COLOR = "#000";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:style="[`max-width: ${size}px; max-height: ${size}px;`, avatar ? `background-color: ${BACKGROUND_COLOR}; border-radius: 9999px; padding: 0.25rem;` : '']">
|
||||
<svg class="w-full h-full" fill="currentColor" fillRule="evenodd" style="flex: none; line-height: 1;"
|
||||
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<div class="inline-flex items-center justify-center"
|
||||
:style="[`width: ${size}px; height: ${size}px;`, avatar ? `background-color: ${BACKGROUND_COLOR}; border-radius: 0.375rem;` : '']">
|
||||
<svg :style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
fill="currentColor" fill-rule="evenodd" style="flex: none; line-height: 1;" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<path
|
||||
d="M9.27 15.29l7.978-5.897c.391-.29.95-.177 1.137.272.98 2.369.542 5.215-1.41 7.169-1.951 1.954-4.667 2.382-7.149 1.406l-2.711 1.257c3.889 2.661 8.611 2.003 11.562-.953 2.341-2.344 3.066-5.539 2.388-8.42l.006.007c-.983-4.232.242-5.924 2.75-9.383.06-.082.12-.164.179-.248l-3.301 3.305v-.01L9.267 15.292M7.623 16.723c-2.792-2.67-2.31-6.801.071-9.184 1.761-1.763 4.647-2.483 7.166-1.425l2.705-1.25a7.808 7.808 0 00-1.829-1A8.975 8.975 0 005.984 5.83c-2.533 2.536-3.33 6.436-1.962 9.764 1.022 2.487-.653 4.246-2.34 6.022-.599.63-1.199 1.259-1.682 1.925l7.62-6.815" />
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
size?: string | number;
|
||||
color?: boolean;
|
||||
avatar?: boolean;
|
||||
}>();
|
||||
|
||||
const TITLE = 'ChatGLM';
|
||||
|
||||
const BACKGROUND_COLOR = "#0053e0";
|
||||
const AVATAR_SCALE = 0.75;
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inline-flex items-center justify-center"
|
||||
:style="[`width: ${size}px; height: ${size}px;`, avatar ? `background: ${BACKGROUND_COLOR}; border-radius: 0.375rem;` : '']">
|
||||
<svg v-if="color && !avatar"
|
||||
:style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<circle cx="12" cy="12" fill="#0055E9" r="12" />
|
||||
<path
|
||||
d="M12 0c.518 0 1.028.033 1.528.096A6.188 6.188 0 0112.12 12.28l-.12.001c-2.99 0-5.242 2.179-5.554 5.11-.223 2.086.353 4.412 2.242 6.146C3.672 22.1 0 17.479 0 12 0 5.373 5.373 0 12 0z"
|
||||
fill="#A8DFF5" />
|
||||
<path
|
||||
d="M5.286 5a2.438 2.438 0 01.682 3.38c-3.962 5.966-3.215 10.743 2.648 15.136C3.636 22.056 0 17.452 0 12c0-1.787.39-3.482 1.09-5.006.253-.435.525-.872.817-1.311A2.438 2.438 0 015.286 5z"
|
||||
fill="#0055E9" />
|
||||
<path
|
||||
d="M12.98.04c.272.021.543.053.81.093.583.106 1.117.254 1.538.44 6.638 2.927 8.07 10.052 1.748 15.642a4.125 4.125 0 01-5.822-.358c-1.51-1.706-1.3-4.184.357-5.822.858-.848 3.108-1.223 4.045-2.441 1.257-1.634 2.122-6.009-2.523-7.506L12.98.039z"
|
||||
fill="#00BCFF" />
|
||||
<path
|
||||
d="M13.528.096A6.187 6.187 0 0112 12.281a5.75 5.75 0 00-1.71.255c.147-.905.595-1.784 1.321-2.501.858-.848 3.108-1.223 4.045-2.441 1.27-1.651 2.14-6.104-2.676-7.554.184.014.367.033.548.056z"
|
||||
fill="#ECECEE" />
|
||||
</svg>
|
||||
<svg v-else fill="currentColor" fill-rule="evenodd" :height="size" style="flex: none; line-height: 1;"
|
||||
:style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
viewBox="0 0 24 24" :width="size" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<path
|
||||
d="M12 0c6.627 0 12 5.373 12 12s-5.373 12-12 12S0 18.627 0 12 5.373 0 12 0zm1.652 1.123l-.01-.001c.533.097 1.023.233 1.41.404 6.084 2.683 7.396 9.214 1.601 14.338a3.781 3.781 0 01-5.337-.328 3.654 3.654 0 01-.884-3.044c-1.934.6-3.295 2.305-3.524 4.45-.204 1.912.324 4.044 2.056 5.634l.245.067C10.1 22.876 11.036 23 12 23c6.075 0 11-4.925 11-11 0-5.513-4.056-10.08-9.348-10.877zM2.748 6.21c-.178.269-.348.536-.51.803l-.235.394.078-.167A10.957 10.957 0 001 12c0 4.919 3.228 9.083 7.682 10.49l.214.065C3.523 18.528 2.84 14.149 6.47 8.68A2.234 2.234 0 102.748 6.21zm10.157-5.172c4.408 1.33 3.61 5.41 2.447 6.924-.86 1.117-2.922 1.46-3.708 2.238-.666.657-1.077 1.462-1.212 2.291A5.303 5.303 0 0112 12.258a5.672 5.672 0 001.404-11.169 10.51 10.51 0 00-.5-.052z" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
size?: string | number;
|
||||
color?: boolean;
|
||||
avatar?: boolean;
|
||||
}>();
|
||||
|
||||
const TITLE = 'IBM';
|
||||
const AVATAR_SCALE = 0.75;
|
||||
|
||||
const BACKGROUND_COLOR = "#0F62FE";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inline-flex items-center justify-center"
|
||||
:style="[`width: ${size}px; height: ${size}px;`, avatar ? `background-color: ${BACKGROUND_COLOR}; border-radius: 0.375rem;` : '']">
|
||||
<svg :style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
fill="currentColor" fill-rule="evenodd" style="flex: none; line-height: 1;" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<path clipRule="evenodd"
|
||||
d="M24 16.333V17h-3.158v-.667H24zm-7.579 0V17h-3.158v-.667h3.158zm2.464 0L18.63 17l-.25-.667h.504zm-7.075 0a2.528 2.528 0 01-1.717.667h-5.04v-.667h6.757zm-7.389 0V17H0v-.667h4.421zm12-1.333v.667h-3.158V15h3.158zm2.958 0l-.246.667h-1L17.885 15h1.494zm-6.937 0c-.057.237-.148.46-.265.667H5.053V15h7.39zm-8.02 0v.667H0V15h4.421zM24 15v.667h-3.158V15H24zm-1.263-1.333v.666h-1.895v-.666h1.895zm-6.316 0v.666h-1.895v-.666h1.895zm3.453 0l-.248.666h-1.989l-.25-.666h2.487zm-7.52 0c.056.212.088.435.088.666h-2.337v-.666h2.249zm-4.143 0v.666H6.316v-.666H8.21zm-5.053 0v.666H1.263v-.666h1.895zm19.579-1.334V13h-1.895v-.667h1.895zm-6.316 0V13h-1.895v-.667h1.895zm3.948 0l-.247.667h-2.987l-.245-.667h3.48zm-8.792 0c.218.188.405.414.55.667H6.315v-.667h5.26zm-8.42 0V13H1.264v-.667h1.895zM18.456 11l.177.539.176-.539h3.929v.667h-1.895v-.613l-.215.613H16.63l-.209-.613v.613h-1.895V11h3.929zM3.158 11v.667H1.263V11h1.895zm8.968 0a2.555 2.555 0 01-.55.667h-5.26V11h5.81zm10.61-1.333v.666h-3.709l.224-.666h3.486zm-4.722 0l.224.666h-3.712v-.666h3.488zm-5.572 0c0 .23-.032.454-.088.666h-2.249v-.666h2.337zm-4.231 0v.666H6.316v-.666H8.21zm-5.053 0v.666H1.263v-.666h1.895zm14.419-1.334l.22.667h-4.534v-.667h4.314zm6.423 0V9h-4.536l.229-.667H24zm-11.823 0c.117.206.208.43.265.667h-7.39v-.667h7.125zm-7.756 0V9H0v-.667h4.421zM17.133 7l.224.667h-4.094V7h3.87zM24 7v.667h-4.089L20.13 7H24zM10.093 7c.662 0 1.264.253 1.717.667H5.053V7h5.04zM4.42 7v.667H0V7h4.421z" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
size?: string | number;
|
||||
color?: boolean;
|
||||
avatar?: boolean;
|
||||
}>();
|
||||
|
||||
const TITLE = 'Inception';
|
||||
const AVATAR_SCALE = 0.7;
|
||||
|
||||
const BACKGROUND_COLOR = "#fff";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inline-flex items-center justify-center"
|
||||
:style="[`width: ${size}px; height: ${size}px;`, avatar ? `background-color: ${BACKGROUND_COLOR}; border-radius: 0.375rem;` : '']">
|
||||
<svg :style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
:fill="avatar ? '#000' : 'currentColor'" fill-rule="evenodd" style="flex: none; line-height: 1;"
|
||||
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<path
|
||||
d="M14.767 1H7.884L1 7.883v6.884h6.884V7.883h6.883V1zM9.234 23h6.882L23 16.116V9.233h-6.884v6.883H9.234V23z" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
size?: string | number;
|
||||
color?: boolean;
|
||||
avatar?: boolean;
|
||||
}>();
|
||||
|
||||
const TITLE = 'Inflection';
|
||||
const AVATAR_SCALE = 0.6;
|
||||
|
||||
const BACKGROUND_COLOR = "#038247";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inline-flex items-center justify-center"
|
||||
:style="[`width: ${size}px; height: ${size}px;`, avatar ? `background-color: ${BACKGROUND_COLOR}; border-radius: 0.375rem;` : '']">
|
||||
<svg :style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
fill="currentColor" fill-rule="evenodd" style="flex: none; line-height: 1;" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<path
|
||||
d="M8.341 24c-.53 0-.841-.308-.841-.824v-.271c0-.514.248-.755.708-.926l1.025-.343c.708-.271.954-.583.954-1.303V3.667c0-.72-.246-1.029-.954-1.303L8.2 2.02c-.46-.171-.701-.408-.701-.926V.824C7.5.309 7.818 0 8.348 0h6.968c.531 0 .85.309.85.824v.271c0 .514-.249.755-.709.926l-1.031.34c-.743.272-.992.583-.992 1.303v16.664c0 .72.249 1.028.992 1.303l1.024.342c.46.172.708.408.708.926v.272c0 .515-.318.824-.85.824L8.342 24z" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,53 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
size?: string | number;
|
||||
color?: boolean;
|
||||
avatar?: boolean;
|
||||
}>();
|
||||
|
||||
const TITLE = 'InternLM';
|
||||
|
||||
const BACKGROUND_COLOR = "#1B3882";
|
||||
const AVATAR_SCALE = 0.75;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inline-flex items-center justify-center"
|
||||
:style="[`width: ${size}px; height: ${size}px;`, avatar ? `background-color: ${BACKGROUND_COLOR}; border-radius: 0.375rem;` : '']">
|
||||
<svg :fill="avatar ? 'currentColor' : ''" fill-rule="evenodd" :height="size" style="flex: none; line-height: 1;"
|
||||
:style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
viewBox="0 0 24 24" :width="size" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<path :fill="color ? '#858599' : ''"
|
||||
d="M5.54 19.662s2.24-.25 2.365-.29c.125-.042 2.45.082 2.45.082l1.493.374 1.37.498.373.125 2.033-.748 2.45-.373 1.659.041 1.286.166.54.166-.042 2.242-1.12-.291-2.159-.166-1.494.124-1.286.291-1.161.374-.83.332-1.744-.664-2.116-.416H7.158l-1.618.291v-2.158z"
|
||||
fillOpacity=".5" />
|
||||
<path :fill="color && !avatar ? '#858599' : ''"
|
||||
d="M13.46 23c-3.773-2.078-7.78-.832-7.82-.819l-.453.144v-2.86l.24-.078c.174-.056 4.324-1.354 8.366.871l-.333.607c-3.244-1.786-6.66-1.115-7.58-.885V21.4c1.203-.286 4.595-.834 7.913.993L13.46 23V23z" />
|
||||
<path :fill="color && !avatar ? '#858599' : ''"
|
||||
d="M13.413 23l-.334-.607c3.319-1.827 6.71-1.279 7.914-.993v-1.419c-.92-.231-4.337-.9-7.58.885l-.334-.607c4.042-2.225 8.192-.927 8.366-.87l.24.077v2.862l-.452-.147c-.04-.013-4.046-1.258-7.82.819z" />
|
||||
<path :fill="color && !avatar ? '#858599' : ''"
|
||||
d="M17.756 16.476a.904.904 0 00-.264-.651.822.822 0 00-.632-.255.997.997 0 00-.644.293c-.175.212-.26.433-.259.685a.874.874 0 00.256.632c.175.187.39.272.644.255.246 0 .459-.1.633-.293a.97.97 0 00.268-.667l-.002.001zM10.769 17.143a.928.928 0 00.264-.658.993.993 0 00-.262-.679.908.908 0 00-.633-.227 1.089 1.089 0 00-.644.274 1.005 1.005 0 00-.265.685c0 .25.089.465.264.643.17.187.383.271.636.255.26-.008.471-.1.639-.293z"
|
||||
fillOpacity=".5" />
|
||||
<path :fill="color && !avatar ? '#858599' : ''"
|
||||
d="M13.4 19.65c-.693 0-1.374-.058-1.895-.192-1.403-.36-2.52-1.01-3.413-1.986-.929-1.017-.988-1.996-1.05-3.033-.008-.133-.016-.267-.026-.405-.083-1.147-.626-2.941-.631-2.958l-.043-.141.086-.119A6.966 6.966 0 019.44 8.441c1.128-.47 2.47-.716 4.105-.752 2.721-.159 5.688.973 7.056 2.692l.086.109-.62 2.89c-.006.155-.052.968-.276 2.63-.263 1.947-2.668 3.061-3.903 3.377-.628.162-1.57.262-2.488.262zm-6.41-8.603c.134.46.538 1.912.613 2.944.01.139.018.277.026.412.06 1.01.109 1.809.896 2.672.815.89 1.837 1.484 3.125 1.814 1.092.28 3.061.194 4.091-.071.887-.228 3.24-1.19 3.468-2.889.24-1.786.272-2.593.273-2.6v-.026l.572-2.664c-.9-1.728-1.433-2.082-2.578-2.514-1.206-.455-2.677.075-3.903.148h-.01c-3.126.068-5.418-.935-6.574 2.773h.001z" />
|
||||
<path :fill="color && !avatar ? '#858599' : ''"
|
||||
d="M19.545 10.862c.223.573.317.86.526 1.386 0 0 .552-.887.727-1.522.175-.641.068-1.458-.07-2.221-.344-1.282-.612-1.381-1.647-2.14a8.668 8.668 0 00-1.565-.84 12.303 12.303 0 00-3.83-.8 12.175 12.175 0 00-3.842.57 8.258 8.258 0 00-1.602.745c-1.075.695-1.823 1.674-2.24 2.932-.112.473-.29.927-.245 1.365.2 2.01 1.512 2.893 1.422 2.409-.117-.625-.042-.994.119-1.592.304-1.13 1.025-1.965 2.163-2.497h.004c.221-.062 1.344-.7 2.834.062.396.17 1.088.629 1.434.618.347-.01.846-.345 1.21-.572.683-.428 1.33-.574 2.184-.303 1.243.39 2.005 1.274 2.418 2.401z" />
|
||||
<path :fill="color && !avatar ? '#858599' : ''"
|
||||
d="M1.217 9.86l.934.274 1.071-.055.77-.522.825-.742.604-.825.77-.797.741-.467 1.016-.22-.687 1.209-.577.576-1.209 1.676-.549.825-.632.44-.907.604s-.797.165-.879.192c-.081.028-.741.138-.741.138l-.577-.028-.44-.137.412-.825.056-.604v-.714l-.001.002z"
|
||||
fillOpacity=".5" />
|
||||
<path :fill="color && !avatar ? '#858599' : ''"
|
||||
d="M3.607 5.108l-.083.63-.22.441-.274.412-.688.412.825.412.907.357.934.193.412.028.99-.688 1.043-.934-.357-.11-1.29-.137-.963-.358-.687-.356-.55-.302z"
|
||||
fillOpacity=".5" />
|
||||
<path :fill="color && !avatar ? '#858599' : ''"
|
||||
d="M1.521 12.399c-.29 0-.596-.018-.917-.051L0 12.278l.298-.19.159-.173c.022-.033.085-.144.19-.377v-.002c.17-.365.228-.975.177-1.813l-.036-.564.435.36c.454.376 1.024.466 1.74.272.64-.182 1.337-.78 2.068-1.774.673-.968 1.64-1.624 2.88-1.952l.116-.031.783.61-.704-.05a2.182 2.182 0 00-.191.206l-.014.017-.018.016c-.404.718-.982 1.426-1.56 2.201l-.008.014c-.089.156-.18.317-.279.477v.009l-.032.051c-.592.962-1.243 1.683-1.935 2.145-.635.445-1.49.67-2.548.67v-.001zm-.5-.505c1.194.077 2.124-.112 2.77-.567l.005-.003c.626-.417 1.223-1.078 1.773-1.964a.244.244 0 01.041-.074c.098-.159.19-.321.28-.48v-.001c.392-.75.842-1.417 1.343-1.988-.748.33-1.341.82-1.8 1.481l-.004.006c-.808 1.101-1.572 1.742-2.333 1.958-.673.182-1.263.15-1.764-.095.01.69-.068 1.207-.237 1.573-.026.058-.05.108-.072.153h-.002z" />
|
||||
<path :fill="color && !avatar ? '#858599' : ''"
|
||||
d="M1.577 12.399c-.29 0-.596-.018-.917-.051l.05-.486c1.371.145 2.425-.035 3.136-.535l.006-.003c.626-.417 1.222-1.078 1.772-1.963a.277.277 0 01.079-.116l.32.37a.22.22 0 00.072-.141l-.005.058-.03.052c-.592.962-1.243 1.683-1.935 2.145-.636.445-1.49.67-2.548.67zM5.751 7.717a9.381 9.381 0 01-.311.016c-.823.029-1.7-.2-2.615-.682.792-.512 1.013-1.096.982-1.621.189.15.386.281.591.393 1.345.758 2.766.878 5.195.198l-.165-.46c-2.308.637-3.558.532-4.793-.165a3.803 3.803 0 01-.967-.759l-.033-.036-.5-.542.094.697c.014.058.3 1.417-1.01 2.08l-.398.201.383.228c1.073.638 2.114.96 3.094.96l.453-.507v-.001zM11.93 15.21a.405.405 0 01-.366-.253c-.326-.745-.582-1.055-.969-1.525-.39-.474-1.223-1.277-1.36-1.393-.184-.156-.236-.368-.126-.515.055-.072.219-.219.556.005.683.452 2.198 1.812 2.608 2.904.15.397 0 .667-.201.749a.367.367 0 01-.143.028h.001zM14.877 15.23a.37.37 0 01-.146-.03c-.201-.085-.346-.357-.192-.752.424-1.086 1.957-2.427 2.645-2.87.34-.22.502-.072.556.002.109.147.054.358-.131.512-.139.115-.982.905-1.379 1.376-.392.465-.651.772-.988 1.513a.404.404 0 01-.364.25v-.002z" />
|
||||
<path :fill="color && !avatar ? '#858599' : ''"
|
||||
d="M16.382 3.316c.01-.566-.265-1.054-.827-1.462-.57-.4-1.252-.602-2.045-.608-.792-.006-1.471.189-2.031.587-.563.401-.85.884-.857 1.445.002.176.017.336.04.486 2.1-.676 3.981-.62 5.64.167.045-.21.072-.414.08-.616v.001z"
|
||||
fillOpacity=".5" />
|
||||
<path :fill="color && !avatar ? '#858599' : ''"
|
||||
d="M7.6 14.716l-.01-.024a1.824 1.824 0 01-.072-.192l-.059-.189-.388.013-.03.326c.002.013.009.044.016.1v.011c.003.02.004.036.008.052a.555.555 0 01-.164.431c-.112.12-.258.172-.45.164-.24-.01-.32-.105-.358-.176-.105-.19-.112-.291-.107-.336.015-.123.04-.254.077-.385.055-.206.013-.444-.126-.708l-.143-.272a1.787 1.787 0 01-.14-.384.974.974 0 01.022-.458.641.641 0 01.264-.374.695.695 0 01.496-.1l.015.002h.015c.192 0 .349.02.469.051a2.208 2.208 0 01-.027-.58 1.5 1.5 0 00-.38-.107H6.52l-.095-.008a1.369 1.369 0 00-.826.189c-.272.16-.456.394-.544.7a1.691 1.691 0 00-.03.815c.008.041.017.09.033.136.043.135.113.285.21.446.148.243.193.323.205.35l.005.009c.018.033.022.084.01.15l-.005.023a7.4 7.4 0 01-.034.172c-.115.298-.127.574-.036.815a.964.964 0 00.486.558c.134.066.288.108.46.125.094.01.192.01.294.005a.957.957 0 00.784-.454 1.13 1.13 0 00.17-.87l-.004-.026H7.6zM21.925 12.059a1.165 1.165 0 00-.556-.697h-.001a1.325 1.325 0 00-.797-.175l-.031.003-.093.011h-.008a1.553 1.553 0 00-.528.197 1.959 1.959 0 01-.016.563l.1-.04c.128-.053.302-.095.532-.13l.013-.002a.567.567 0 01.474.103c.155.113.26.254.32.428.05.164.057.305.022.43a2.77 2.77 0 01-.144.41 9.169 9.169 0 01-.191.383.908.908 0 00-.102.57c.024.176.05.322.078.446a.608.608 0 01-.036.329l-.003.006c-.037.117-.235.18-.35.208-.263.063-.381-.033-.451-.12a.78.78 0 01-.173-.51l.004-.053a.806.806 0 01.014-.093l-.008-.337-.431.01-.046.185a1.285 1.285 0 01-.064.188l-.011.027-.005.029c-.05.307.013.598.187.868l.004.006c.19.274.463.426.787.437.087.005.17.002.25-.004.192-.018.363-.069.509-.15a.992.992 0 00.459-.567c.085-.252.068-.527-.05-.816l-.03-.171-.002-.013a.306.306 0 01.015-.18c.045-.108.106-.233.181-.375.098-.173.162-.32.196-.444a.665.665 0 00.033-.152 1.75 1.75 0 00-.053-.806l.002-.002zM16.59 3.853c.033-.185.053-.359.06-.533.012-.648-.301-1.209-.93-1.667-.609-.427-1.344-.646-2.184-.653-.843-.008-1.575.207-2.176.633-.628.45-.951 1.002-.96 1.643v.006c.002.128.01.256.026.39a.38.38 0 00-.095.23v.01c0 .216.032.433.097.643l.001.006c.09.265.245.518.46.75l.063.068.567.063c.485.282 1.132.433 1.928.447h.103c.587 0 1.1-.072 1.527-.213l.011-.004c.16-.063.316-.138.465-.224.452.043.791-.18.965-.633l.043-.046-.002-.065c.067-.205.184-.608.03-.851h.001zM11.643 2.03c.514-.365 1.149-.547 1.888-.542.74.006 1.38.196 1.903.562.497.361.735.774.727 1.255-.004.089-.01.18-.023.274a6.882 6.882 0 00-2.685-.527c-.816 0-1.67.13-2.558.39a4.038 4.038 0 01-.006-.165c.006-.482.253-.89.754-1.247zm4.513 2.293l-.165.357s-.215.311-.45.272l-.092-.015-.078.049a2.724 2.724 0 01-.454.226c-.397.13-.89.192-1.462.184-.708-.014-1.276-.14-1.688-.38l-.037-.021-.048-.03-.491-.054c-.094-.11-.226-.34-.226-.34l-.135-.392-.008-.204c1.988-.626 3.79-.574 5.355.155.002.029-.02.193-.02.193zm.147.364v-.004l.003.004h-.003z" />
|
||||
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,51 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
size?: string | number;
|
||||
color?: boolean;
|
||||
avatar?: boolean;
|
||||
}>();
|
||||
|
||||
const TITLE = 'Kwaipilot';
|
||||
|
||||
const BACKGROUND_COLOR = "#000";
|
||||
const AVATAR_SCALE = 0.8;
|
||||
|
||||
const [a, b] = useFillIds(TITLE, 2);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inline-flex items-center justify-center"
|
||||
:style="[`width: ${size}px; height: ${size}px;`, avatar ? `background: ${BACKGROUND_COLOR}; border-radius: 0.375rem;` : '']">
|
||||
<svg v-if="color"
|
||||
:style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<path
|
||||
d="M11.765.03C5.327.03.108 5.25.108 11.686c0 3.514 1.556 6.665 4.015 8.804L9.89 8.665h6.451L9.31 23.083c.807.173 1.63.26 2.455.26 6.438 0 11.657-5.22 11.657-11.658S18.202.028 11.765.028V.03z"
|
||||
:fill="a!.fill" />
|
||||
<path
|
||||
d="M4.123 20.489l6.362-13.046c.017-.036.035-.073.055-.11l.086-.18h.005a6.697 6.697 0 015.913-3.551c2.784 0 5.171 1.7 6.184 4.116-1.622-4.485-5.92-7.69-10.963-7.69C5.327.028.108 5.247.108 11.685c0 3.514 1.556 6.666 4.015 8.804z"
|
||||
:fill="b!.fill" />
|
||||
|
||||
<defs>
|
||||
<linearGradient gradientUnits="userSpaceOnUse" :id="a!.id" x1="13.469" x2="12.557" y1="4.823"
|
||||
y2="21.302">
|
||||
<stop offset=".313" stop-color="#9EC0E0" />
|
||||
<stop offset="1" stop-color="#fff" />
|
||||
</linearGradient>
|
||||
<linearGradient gradientUnits="userSpaceOnUse" :id="b!.id" x1="13.739" x2="5.647" y1="4.229"
|
||||
y2="17.386">
|
||||
<stop stop-color="#fff" />
|
||||
<stop offset="1" stop-color="#BCD5EC" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
<svg v-else fill="currentColor" fill-rule="evenodd" :height="size" style="flex: none; line-height: 1;"
|
||||
:style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
viewBox="0 0 24 24" :width="size" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<path clipRule="evenodd"
|
||||
d="M11.765.03C5.327.03.108 5.25.108 11.686c0 3.514 1.556 6.665 4.015 8.804L9.89 8.665h6.451L9.31 23.083c.807.173 1.63.26 2.455.26 6.438 0 11.657-5.22 11.657-11.658S18.202.028 11.765.028V.03z" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,24 @@
|
||||
All logo SVGs sourced from https://github.com/lobehub/lobe-icons under the MIT
|
||||
license.
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 LobeHub
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
size?: string | number;
|
||||
color?: boolean;
|
||||
avatar?: boolean;
|
||||
}>();
|
||||
|
||||
const TITLE = 'Liquid';
|
||||
const AVATAR_SCALE = 0.75;
|
||||
|
||||
const BACKGROUND_COLOR = "#fff";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inline-flex items-center justify-center"
|
||||
:style="[`width: ${size}px; height: ${size}px;`, avatar ? `background-color: ${BACKGROUND_COLOR}; border-radius: 0.375rem;` : '']">
|
||||
<svg :style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
:fill="avatar ? '#000' : 'currentColor'" fill-rule="evenodd" style="flex: none; line-height: 1;"
|
||||
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<path
|
||||
d="M12.028 8.546l-.008.005 3.03 5.25a3.94 3.94 0 01.643 2.162c0 .754-.212 1.46-.58 2.062l6.173-1.991L11.63 0 9.304 3.872l2.724 4.674zM6.837 24l4.85-4.053h-.013c-2.219 0-4.017-1.784-4.017-3.984 0-.794.235-1.534.64-2.156l2.865-4.976-2.381-4.087L2 16.034 6.83 24h.007zM13.737 19.382h-.001L8.222 24h8.182l4.148-6.769-6.815 2.151z" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
size?: string | number;
|
||||
color?: boolean;
|
||||
avatar?: boolean;
|
||||
}>();
|
||||
|
||||
const TITLE = 'LongCat';
|
||||
|
||||
const BACKGROUND_COLOR = "#fff";
|
||||
const AVATAR_SCALE = 0.7;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inline-flex items-center justify-center"
|
||||
:style="[`width: ${size}px; height: ${size}px;`, avatar ? `background-color: ${BACKGROUND_COLOR}; border-radius: 0.375rem;` : '']">
|
||||
<svg :fill="avatar ? '#000' : 'currentColor'" fill-rule="evenodd" :height="size"
|
||||
style="flex: none; line-height: 1;"
|
||||
:style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
viewBox="0 0 24 24" :width="size" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<path clipRule="evenodd" :fill="color ? '#29E154' : ''"
|
||||
d="M.507 19.883a.507.507 0 01-.489-.642L4.29 3.745a1.013 1.013 0 011.533-.578l5.622 3.687a1.013 1.013 0 001.11 0L18.2 3.165a1.013 1.013 0 011.532.58l4.25 15.497a.506.506 0 01-.49.64H18.07a6.297 6.297 0 001.53-4.115v-.177a6.09 6.09 0 00-1.513-4.017l-.697-3.495a.438.438 0 00-.694-.266L14.07 9.781a.748.748 0 01-.654.121 5.156 5.156 0 00-2.833 0 .746.746 0 01-.653-.121L7.302 7.81a.435.435 0 00-.688.269l-.675 3.652a5.36 5.36 0 00-1.539 3.76v.333c0 1.474.527 2.9 1.488 4.02l.032.038H.507z" />
|
||||
<path d="M9.213 16.843h1.52v-3.546h-1.29l-.23 3.546zm5.573 0h-1.52v-3.546h1.29l.23 3.546z" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,132 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
size?: string | number;
|
||||
color?: boolean;
|
||||
avatar?: boolean;
|
||||
}>();
|
||||
|
||||
const TITLE = 'Gemma';
|
||||
|
||||
const BACKGROUND_COLOR = "linear-gradient(45deg, #007FF8, #0668E1, #007FF8)";
|
||||
const AVATAR_SCALE = 0.75;
|
||||
|
||||
const [a, b, c, d, e, f, g, h, i, j, k, l, m] = useFillIds(TITLE, 13);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inline-flex items-center justify-center"
|
||||
:style="[`width: ${size}px; height: ${size}px;`, avatar ? `background: ${BACKGROUND_COLOR}; border-radius: 0.375rem;` : '']">
|
||||
<svg v-if="color && !avatar"
|
||||
:style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<path
|
||||
d="M6.897 4h-.024l-.031 2.615h.022c1.715 0 3.046 1.357 5.94 6.246l.175.297.012.02 1.62-2.438-.012-.019a48.763 48.763 0 00-1.098-1.716 28.01 28.01 0 00-1.175-1.629C10.413 4.932 8.812 4 6.896 4z"
|
||||
:fill="a!.fill" />
|
||||
<path
|
||||
d="M6.873 4C4.95 4.01 3.247 5.258 2.02 7.17a4.352 4.352 0 00-.01.017l2.254 1.231.011-.017c.718-1.083 1.61-1.774 2.568-1.785h.021L6.896 4h-.023z"
|
||||
:fill="b!.fill" />
|
||||
<path
|
||||
d="M2.019 7.17l-.011.017C1.2 8.447.598 9.995.274 11.664l-.005.022 2.534.6.004-.022c.27-1.467.786-2.828 1.456-3.845l.011-.017L2.02 7.17z"
|
||||
:fill="c!.fill" />
|
||||
<path
|
||||
d="M2.807 12.264l-2.533-.6-.005.022c-.177.918-.267 1.851-.269 2.786v.023l2.598.233v-.023a12.591 12.591 0 01.21-2.44z"
|
||||
:fill="d!.fill" />
|
||||
<path
|
||||
d="M2.677 15.537a5.462 5.462 0 01-.079-.813v-.022L0 14.468v.024a8.89 8.89 0 00.146 1.652l2.535-.585a4.106 4.106 0 01-.004-.022z"
|
||||
:fill="e!.fill" />
|
||||
<path
|
||||
d="M3.27 16.89c-.284-.31-.484-.756-.589-1.328l-.004-.021-2.535.585.004.021c.192 1.01.568 1.85 1.106 2.487l.014.017 2.018-1.745a2.106 2.106 0 01-.015-.016z"
|
||||
:fill="f!.fill" />
|
||||
<path
|
||||
d="M10.78 9.654c-1.528 2.35-2.454 3.825-2.454 3.825-2.035 3.2-2.739 3.917-3.871 3.917a1.545 1.545 0 01-1.186-.508l-2.017 1.744.014.017C2.01 19.518 3.058 20 4.356 20c1.963 0 3.374-.928 5.884-5.33l1.766-3.13a41.283 41.283 0 00-1.227-1.886z"
|
||||
fill="#0082FB" />
|
||||
<path
|
||||
d="M13.502 5.946l-.016.016c-.4.43-.786.908-1.16 1.416.378.483.768 1.024 1.175 1.63.48-.743.928-1.345 1.367-1.807l.016-.016-1.382-1.24z"
|
||||
:fill="g!.fill" />
|
||||
<path
|
||||
d="M20.918 5.713C19.853 4.633 18.583 4 17.225 4c-1.432 0-2.637.787-3.723 1.944l-.016.016 1.382 1.24.016-.017c.715-.747 1.408-1.12 2.176-1.12.826 0 1.6.39 2.27 1.075l.015.016 1.589-1.425-.016-.016z"
|
||||
fill="#0082FB" />
|
||||
<path
|
||||
d="M23.998 14.125c-.06-3.467-1.27-6.566-3.064-8.396l-.016-.016-1.588 1.424.015.016c1.35 1.392 2.277 3.98 2.361 6.971v.023h2.292v-.022z"
|
||||
:fill="h!.fill" />
|
||||
<path
|
||||
d="M23.998 14.15v-.023h-2.292v.022c.004.14.006.282.006.424 0 .815-.121 1.474-.368 1.95l-.011.022 1.708 1.782.013-.02c.62-.96.946-2.293.946-3.91 0-.083 0-.165-.002-.247z"
|
||||
:fill="i!.fill" />
|
||||
<path
|
||||
d="M21.344 16.52l-.011.02c-.214.402-.519.67-.917.787l.778 2.462a3.493 3.493 0 00.438-.182 3.558 3.558 0 001.366-1.218l.044-.065.012-.02-1.71-1.784z"
|
||||
:fill="j!.fill" />
|
||||
<path
|
||||
d="M19.92 17.393c-.262 0-.492-.039-.718-.14l-.798 2.522c.449.153.927.222 1.46.222.492 0 .943-.073 1.352-.215l-.78-2.462c-.167.05-.341.075-.517.073z"
|
||||
:fill="k!.fill" />
|
||||
<path
|
||||
d="M18.323 16.534l-.014-.017-1.836 1.914.016.017c.637.682 1.246 1.105 1.937 1.337l.797-2.52c-.291-.125-.573-.353-.9-.731z"
|
||||
:fill="l!.fill" />
|
||||
<path
|
||||
d="M18.309 16.515c-.55-.642-1.232-1.712-2.303-3.44l-1.396-2.336-.011-.02-1.62 2.438.012.02.989 1.668c.959 1.61 1.74 2.774 2.493 3.585l.016.016 1.834-1.914a2.353 2.353 0 01-.014-.017z"
|
||||
:fill="m!.fill" />
|
||||
<defs>
|
||||
<linearGradient id={a.id} x1="75.897%" x2="26.312%" y1="89.199%" y2="12.194%">
|
||||
<stop offset=".06%" stopColor="#0867DF" />
|
||||
<stop offset="45.39%" stopColor="#0668E1" />
|
||||
<stop offset="85.91%" stopColor="#0064E0" />
|
||||
</linearGradient>
|
||||
<linearGradient :id="b!.id" x1="21.67%" x2="97.068%" y1="75.874%" y2="23.985%">
|
||||
<stop offset="13.23%" stopColor="#0064DF" />
|
||||
<stop offset="99.88%" stopColor="#0064E0" />
|
||||
</linearGradient>
|
||||
<linearGradient :id="c!.id" x1="38.263%" x2="60.895%" y1="89.127%" y2="16.131%">
|
||||
<stop offset="1.47%" stopColor="#0072EC" />
|
||||
<stop offset="68.81%" stopColor="#0064DF" />
|
||||
</linearGradient>
|
||||
<linearGradient :id="d!.id" x1="47.032%" x2="52.15%" y1="90.19%" y2="15.745%">
|
||||
<stop offset="7.31%" stopColor="#007CF6" />
|
||||
<stop offset="99.43%" stopColor="#0072EC" />
|
||||
</linearGradient>
|
||||
<linearGradient :id="e!.id" x1="52.155%" x2="47.591%" y1="58.301%" y2="37.004%">
|
||||
<stop offset="7.31%" stopColor="#007FF9" />
|
||||
<stop offset="100%" stopColor="#007CF6" />
|
||||
</linearGradient>
|
||||
<linearGradient :id="f!.id" x1="37.689%" x2="61.961%" y1="12.502%" y2="63.624%">
|
||||
<stop offset="7.31%" stopColor="#007FF9" />
|
||||
<stop offset="100%" stopColor="#0082FB" />
|
||||
</linearGradient>
|
||||
<linearGradient :id="g!.id" x1="34.808%" x2="62.313%" y1="68.859%" y2="23.174%">
|
||||
<stop offset="27.99%" stopColor="#007FF8" />
|
||||
<stop offset="91.41%" stopColor="#0082FB" />
|
||||
</linearGradient>
|
||||
<linearGradient :id="h!.id" x1="43.762%" x2="57.602%" y1="6.235%" y2="98.514%">
|
||||
<stop offset="0%" stopColor="#0082FB" />
|
||||
<stop offset="99.95%" stopColor="#0081FA" />
|
||||
</linearGradient>
|
||||
<linearGradient :id="i!.id" x1="60.055%" x2="39.88%" y1="4.661%" y2="69.077%">
|
||||
<stop offset="6.19%" stopColor="#0081FA" />
|
||||
<stop offset="100%" stopColor="#0080F9" />
|
||||
</linearGradient>
|
||||
<linearGradient :id="j!.id" x1="30.282%" x2="61.081%" y1="59.32%" y2="33.244%">
|
||||
<stop offset="0%" stopColor="#027AF3" />
|
||||
<stop offset="100%" stopColor="#0080F9" />
|
||||
</linearGradient>
|
||||
<linearGradient :id="k!.id" x1="20.433%" x2="82.112%" y1="50.001%" y2="50.001%">
|
||||
<stop offset="0%" stopColor="#0377EF" />
|
||||
<stop offset="99.94%" stopColor="#0279F1" />
|
||||
</linearGradient>
|
||||
<linearGradient :id="l!.id" x1="40.303%" x2="72.394%" y1="35.298%" y2="57.811%">
|
||||
<stop offset=".19%" stopColor="#0471E9" />
|
||||
<stop offset="100%" stopColor="#0377EF" />
|
||||
</linearGradient>
|
||||
<linearGradient :id="m!.id" x1="32.254%" x2="68.003%" y1="19.719%" y2="84.908%">
|
||||
<stop offset="27.65%" stopColor="#0867DF" />
|
||||
<stop offset="100%" stopColor="#0471E9" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
<svg v-else fill="currentColor" fill-rule="evenodd" :height="size" style="flex: none; line-height: 1;"
|
||||
:style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
viewBox="0 0 24 24" :width="size" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<path
|
||||
d="M6.897 4c1.915 0 3.516.932 5.43 3.376l.282-.373c.19-.246.383-.484.58-.71l.313-.35C14.588 4.788 15.792 4 17.225 4c1.273 0 2.469.557 3.491 1.516l.218.213c1.73 1.765 2.917 4.71 3.053 8.026l.011.392.002.25c0 1.501-.28 2.759-.818 3.7l-.14.23-.108.153c-.301.42-.664.758-1.086 1.009l-.265.142-.087.04a3.493 3.493 0 01-.302.118 4.117 4.117 0 01-1.33.208c-.524 0-.996-.067-1.438-.215-.614-.204-1.163-.56-1.726-1.116l-.227-.235c-.753-.812-1.534-1.976-2.493-3.586l-1.43-2.41-.544-.895-1.766 3.13-.343.592C7.597 19.156 6.227 20 4.356 20c-1.21 0-2.205-.42-2.936-1.182l-.168-.184c-.484-.573-.837-1.311-1.043-2.189l-.067-.32a8.69 8.69 0 01-.136-1.288L0 14.468c.002-.745.06-1.49.174-2.23l.1-.573c.298-1.53.828-2.958 1.536-4.157l.209-.34c1.177-1.83 2.789-3.053 4.615-3.16L6.897 4zm-.033 2.615l-.201.01c-.83.083-1.606.673-2.252 1.577l-.138.199-.01.018c-.67 1.017-1.185 2.378-1.456 3.845l-.004.022a12.591 12.591 0 00-.207 2.254l.002.188c.004.18.017.36.04.54l.043.291c.092.503.257.908.486 1.208l.117.137c.303.323.698.492 1.17.492 1.1 0 1.796-.676 3.696-3.641l2.175-3.4.454-.701-.139-.198C9.11 7.3 8.084 6.616 6.864 6.616zm10.196-.552l-.176.007c-.635.048-1.223.359-1.82.933l-.196.198c-.439.462-.887 1.064-1.367 1.807l.266.398c.18.274.362.56.55.858l.293.475 1.396 2.335.695 1.114c.583.926 1.03 1.6 1.408 2.082l.213.262c.282.326.529.54.777.673l.102.05c.227.1.457.138.718.138.176.002.35-.023.518-.073.338-.104.61-.32.813-.637l.095-.163.077-.162c.194-.459.29-1.06.29-1.785l-.006-.449c-.08-2.871-.938-5.372-2.2-6.798l-.176-.189c-.67-.683-1.444-1.074-2.27-1.074z" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
size?: string | number;
|
||||
color?: boolean;
|
||||
avatar?: boolean;
|
||||
}>();
|
||||
|
||||
const TITLE = 'Microsoft';
|
||||
|
||||
const BACKGROUND_COLOR = "#2468f2";
|
||||
const AVATAR_SCALE = 0.6;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inline-flex items-center justify-center"
|
||||
:style="[`width: ${size}px; height: ${size}px;`, avatar ? `background-color: ${BACKGROUND_COLOR}; border-radius: 0.375rem;` : '']">
|
||||
<svg :fill="avatar ? 'currentColor' : ''" fill-rule="evenodd" :height="size" style="flex: none; line-height: 1;"
|
||||
:style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
viewBox="0 0 24 24" :width="size" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<path d="M11.49 2H2v9.492h9.492V2h-.002z" :fill="!avatar && color ? '#F25022' : ''" />
|
||||
<path d="M22 2h-9.492v9.492H22V2z" :fill="!avatar && color ? '#7FBA00' : ''" />
|
||||
<path d="M11.49 12.508H2V22h9.492v-9.492h-.002z" :fill="!avatar && color ? '#00A4EF' : ''" />
|
||||
<path d="M22 12.508h-9.492V22H22v-9.492z" :fill="!avatar && color ? '#FFB900' : ''" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
size?: string | number;
|
||||
color?: boolean;
|
||||
avatar?: boolean;
|
||||
}>();
|
||||
|
||||
const TITLE = 'Minimax';
|
||||
|
||||
const BACKGROUND_COLOR = "linear-gradient(to right, #E2167E, #FE603C)";
|
||||
const AVATAR_SCALE = 0.75;
|
||||
|
||||
const [fill] = useFillIds(TITLE, 1);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inline-flex items-center justify-center"
|
||||
:style="[`width: ${size}px; height: ${size}px;`, avatar ? `background: ${BACKGROUND_COLOR}; border-radius: 0.375rem;` : '']">
|
||||
<svg v-if="color && !avatar"
|
||||
:style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<defs>
|
||||
<linearGradient :id="fill!.id" x1="0%" x2="100.182%" y1="50.057%" y2="50.057%">
|
||||
<stop offset="0%" stopColor="#E2167E" />
|
||||
<stop offset="100%" stopColor="#FE603C" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path
|
||||
d="M16.278 2c1.156 0 2.093.927 2.093 2.07v12.501a.74.74 0 00.744.709.74.74 0 00.743-.709V9.099a2.06 2.06 0 012.071-2.049A2.06 2.06 0 0124 9.1v6.561a.649.649 0 01-.652.645.649.649 0 01-.653-.645V9.1a.762.762 0 00-.766-.758.762.762 0 00-.766.758v7.472a2.037 2.037 0 01-2.048 2.026 2.037 2.037 0 01-2.048-2.026v-12.5a.785.785 0 00-.788-.753.785.785 0 00-.789.752l-.001 15.904A2.037 2.037 0 0113.441 22a2.037 2.037 0 01-2.048-2.026V18.04c0-.356.292-.645.652-.645.36 0 .652.289.652.645v1.934c0 .263.142.506.372.638.23.131.514.131.744 0a.734.734 0 00.372-.638V4.07c0-1.143.937-2.07 2.093-2.07zm-5.674 0c1.156 0 2.093.927 2.093 2.07v11.523a.648.648 0 01-.652.645.648.648 0 01-.652-.645V4.07a.785.785 0 00-.789-.78.785.785 0 00-.789.78v14.013a2.06 2.06 0 01-2.07 2.048 2.06 2.06 0 01-2.071-2.048V9.1a.762.762 0 00-.766-.758.762.762 0 00-.766.758v3.8a2.06 2.06 0 01-2.071 2.049A2.06 2.06 0 010 12.9v-1.378c0-.357.292-.646.652-.646.36 0 .653.29.653.646V12.9c0 .418.343.757.766.757s.766-.339.766-.757V9.099a2.06 2.06 0 012.07-2.048 2.06 2.06 0 012.071 2.048v8.984c0 .419.343.758.767.758.423 0 .766-.339.766-.758V4.07c0-1.143.937-2.07 2.093-2.07z"
|
||||
:fill="fill!.fill" fillRule="nonzero" />
|
||||
</svg>
|
||||
<svg v-else fill="currentColor" fill-rule="evenodd" :height="size" style="flex: none; line-height: 1;"
|
||||
:style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
viewBox="0 0 24 24" :width="size" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<path
|
||||
d="M16.278 2c1.156 0 2.093.927 2.093 2.07v12.501a.74.74 0 00.744.709.74.74 0 00.743-.709V9.099a2.06 2.06 0 012.071-2.049A2.06 2.06 0 0124 9.1v6.561a.649.649 0 01-.652.645.649.649 0 01-.653-.645V9.1a.762.762 0 00-.766-.758.762.762 0 00-.766.758v7.472a2.037 2.037 0 01-2.048 2.026 2.037 2.037 0 01-2.048-2.026v-12.5a.785.785 0 00-.788-.753.785.785 0 00-.789.752l-.001 15.904A2.037 2.037 0 0113.441 22a2.037 2.037 0 01-2.048-2.026V18.04c0-.356.292-.645.652-.645.36 0 .652.289.652.645v1.934c0 .263.142.506.372.638.23.131.514.131.744 0a.734.734 0 00.372-.638V4.07c0-1.143.937-2.07 2.093-2.07zm-5.674 0c1.156 0 2.093.927 2.093 2.07v11.523a.648.648 0 01-.652.645.648.648 0 01-.652-.645V4.07a.785.785 0 00-.789-.78.785.785 0 00-.789.78v14.013a2.06 2.06 0 01-2.07 2.048 2.06 2.06 0 01-2.071-2.048V9.1a.762.762 0 00-.766-.758.762.762 0 00-.766.758v3.8a2.06 2.06 0 01-2.071 2.049A2.06 2.06 0 010 12.9v-1.378c0-.357.292-.646.652-.646.36 0 .653.29.653.646V12.9c0 .418.343.757.766.757s.766-.339.766-.757V9.099a2.06 2.06 0 012.07-2.048 2.06 2.06 0 012.071 2.048v8.984c0 .419.343.758.767.758.423 0 .766-.339.766-.758V4.07c0-1.143.937-2.07 2.093-2.07z" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,37 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
size?: string | number;
|
||||
color?: boolean;
|
||||
avatar?: boolean;
|
||||
}>();
|
||||
|
||||
const TITLE = 'Mistral';
|
||||
|
||||
const BACKGROUND_COLOR = "#FA520F";
|
||||
const AVATAR_SCALE = 0.75;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inline-flex items-center justify-center"
|
||||
:style="[`width: ${size}px; height: ${size}px;`, avatar ? `background-color: ${BACKGROUND_COLOR}; border-radius: 0.375rem;` : '']">
|
||||
<svg v-if="color && !avatar"
|
||||
:style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<path d="M3.428 3.4h3.429v3.428H3.428V3.4zm13.714 0h3.43v3.428h-3.43V3.4z" fill="gold" />
|
||||
<path d="M3.428 6.828h6.857v3.429H3.429V6.828zm10.286 0h6.857v3.429h-6.857V6.828z" fill="#FFAF00" />
|
||||
<path d="M3.428 10.258h17.144v3.428H3.428v-3.428z" fill="#FF8205" />
|
||||
<path
|
||||
d="M3.428 13.686h3.429v3.428H3.428v-3.428zm6.858 0h3.429v3.428h-3.429v-3.428zm6.856 0h3.43v3.428h-3.43v-3.428z"
|
||||
fill="#FA500F" />
|
||||
<path d="M0 17.114h10.286v3.429H0v-3.429zm13.714 0H24v3.429H13.714v-3.429z" fill="#E10500" />
|
||||
</svg>
|
||||
<svg v-else fill="currentColor" fill-rule="evenodd" :height="size" style="flex: none; line-height: 1;"
|
||||
:style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
viewBox="0 0 24 24" :width="size" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<path clip-rule="evenodd"
|
||||
d="M3.428 3.4h3.429v3.428h3.429v3.429h-.002 3.431V6.828h3.427V3.4h3.43v13.714H24v3.429H13.714v-3.428h-3.428v-3.429h-3.43v3.428h3.43v3.429H0v-3.429h3.428V3.4zm10.286 13.715h3.428v-3.429h-3.427v3.429z" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
size?: string | number;
|
||||
color?: boolean;
|
||||
avatar?: boolean;
|
||||
}>();
|
||||
|
||||
const TITLE = 'Moonshot';
|
||||
const AVATAR_SCALE = 0.75;
|
||||
|
||||
const BACKGROUND_COLOR = "#16191E";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inline-flex items-center justify-center"
|
||||
:style="[`width: ${size}px; height: ${size}px;`, avatar ? `background-color: ${BACKGROUND_COLOR}; border-radius: 0.375rem;` : '']">
|
||||
<svg :style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
fill="currentColor" fill-rule="evenodd" style="flex: none; line-height: 1;" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<path
|
||||
d="M1.052 16.916l9.539 2.552a21.007 21.007 0 00.06 2.033l5.956 1.593a11.997 11.997 0 01-5.586.865l-.18-.016-.044-.004-.084-.009-.094-.01a11.605 11.605 0 01-.157-.02l-.107-.014-.11-.016a11.962 11.962 0 01-.32-.051l-.042-.008-.075-.013-.107-.02-.07-.015-.093-.019-.075-.016-.095-.02-.097-.023-.094-.022-.068-.017-.088-.022-.09-.024-.095-.025-.082-.023-.109-.03-.062-.02-.084-.025-.093-.028-.105-.034-.058-.019-.08-.026-.09-.031-.066-.024a6.293 6.293 0 01-.044-.015l-.068-.025-.101-.037-.057-.022-.08-.03-.087-.035-.088-.035-.079-.032-.095-.04-.063-.028-.063-.027a5.655 5.655 0 01-.041-.018l-.066-.03-.103-.047-.052-.024-.096-.046-.062-.03-.084-.04-.086-.044-.093-.047-.052-.027-.103-.055-.057-.03-.058-.032a6.49 6.49 0 01-.046-.026l-.094-.053-.06-.034-.051-.03-.072-.041-.082-.05-.093-.056-.052-.032-.084-.053-.061-.039-.079-.05-.07-.047-.053-.035a7.785 7.785 0 01-.054-.036l-.044-.03-.044-.03a6.066 6.066 0 01-.04-.028l-.057-.04-.076-.054-.069-.05-.074-.054-.056-.042-.076-.057-.076-.059-.086-.067-.045-.035-.064-.052-.074-.06-.089-.073-.046-.039-.046-.039a7.516 7.516 0 01-.043-.037l-.045-.04-.061-.053-.07-.062-.068-.06-.062-.058-.067-.062-.053-.05-.088-.084a13.28 13.28 0 01-.099-.097l-.029-.028-.041-.042-.069-.07-.05-.051-.05-.053a6.457 6.457 0 01-.168-.179l-.08-.088-.062-.07-.071-.08-.042-.049-.053-.062-.058-.068-.046-.056a7.175 7.175 0 01-.027-.033l-.045-.055-.066-.082-.041-.052-.05-.064-.02-.025a11.99 11.99 0 01-1.44-2.402zm-1.02-5.794l11.353 3.037a20.468 20.468 0 00-.469 2.011l10.817 2.894a12.076 12.076 0 01-1.845 2.005L.657 15.923l-.016-.046-.035-.104a11.965 11.965 0 01-.05-.153l-.007-.023a11.896 11.896 0 01-.207-.741l-.03-.126-.018-.08-.021-.097-.018-.081-.018-.09-.017-.084-.018-.094c-.026-.141-.05-.283-.071-.426l-.017-.118-.011-.083-.013-.102a12.01 12.01 0 01-.019-.161l-.005-.047a12.12 12.12 0 01-.034-2.145zm1.593-5.15l11.948 3.196c-.368.605-.705 1.231-1.01 1.875l11.295 3.022c-.142.82-.368 1.612-.668 2.365l-11.55-3.09L.124 10.26l.015-.1.008-.049.01-.067.015-.087.018-.098c.026-.148.056-.295.088-.442l.028-.124.02-.085.024-.097c.022-.09.045-.18.07-.268l.028-.102.023-.083.03-.1.025-.082.03-.096.026-.082.031-.095a11.896 11.896 0 011.01-2.232zm4.442-4.4L17.352 4.59a20.77 20.77 0 00-1.688 1.721l7.823 2.093c.267.852.442 1.744.513 2.665L2.106 5.213l.045-.065.027-.04.04-.055.046-.065.055-.076.054-.072.064-.086.05-.065.057-.073.055-.07.06-.074.055-.069.065-.077.054-.066.066-.077.053-.06.072-.082.053-.06.067-.074.054-.058.073-.078.058-.06.063-.067.168-.17.1-.098.059-.056.076-.071a12.084 12.084 0 012.272-1.677zM12.017 0h.097l.082.001.069.001.054.002.068.002.046.001.076.003.047.002.06.003.054.002.087.005.105.007.144.011.088.007.044.004.077.008.082.008.047.005.102.012.05.006.108.014.081.01.042.006.065.01.207.032.07.012.065.011.14.026.092.018.11.022.046.01.075.016.041.01L14.7.3l.042.01.065.015.049.012.071.017.096.024.112.03.113.03.113.032.05.015.07.02.078.024.073.023.05.016.05.016.076.025.099.033.102.036.048.017.064.023.093.034.11.041.116.045.1.04.047.02.06.024.041.018.063.026.04.018.057.025.11.048.1.046.074.035.075.036.06.028.092.046.091.045.102.052.053.028.049.026.046.024.06.033.041.022.052.029.088.05.106.06.087.051.057.034.053.032.096.059.088.055.098.062.036.024.064.041.084.056.04.027.062.042.062.043.023.017c.054.037.108.075.161.114l.083.06.065.048.056.043.086.065.082.064.04.03.05.041.086.069.079.065.085.071c.712.6 1.353 1.283 1.909 2.031L7.222.994l.062-.027.065-.028.081-.034.086-.035c.113-.045.227-.09.341-.131l.096-.035.093-.033.084-.03.096-.031c.087-.03.176-.058.264-.085l.091-.027.086-.025.102-.03.085-.023.1-.026L9.04.37l.09-.023.091-.022.095-.022.09-.02.098-.021.091-.02.095-.018.092-.018.1-.018.091-.016.098-.017.092-.014.097-.015.092-.013.102-.013.091-.012.105-.012.09-.01.105-.01c.093-.01.186-.018.28-.024l.106-.008.09-.005.11-.006.093-.004.1-.004.097-.002.099-.002.197-.002z" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
size?: string | number;
|
||||
color?: boolean;
|
||||
avatar?: boolean;
|
||||
}>();
|
||||
|
||||
const TITLE = 'Morph';
|
||||
|
||||
const BACKGROUND_COLOR = "#000";
|
||||
const AVATAR_SCALE = 0.7;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inline-flex items-center justify-center"
|
||||
:style="[`width: ${size}px; height: ${size}px;`, avatar ? `background-color: ${BACKGROUND_COLOR}; border-radius: 0.375rem;` : '']">
|
||||
<svg :style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
:fill="avatar || color ? '#99d52a' : 'currentColor'" :fill-rule="color && !avatar ? 'evenodd' : 'nonzero'"
|
||||
style="flex: none; line-height: 1;" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<path
|
||||
d="M7.941 2c.23 0 .452.073.638.21.186.136.325.328.397.55l.593 1.814c.073.221.212.413.397.55.186.136.409.21.638.21h2.791c.23 0 .452-.074.638-.21a1.11 1.11 0 00.397-.55l.594-1.815a1.11 1.11 0 01.397-.55c.185-.136.408-.209.637-.209h1.7c.23 0 .453.073.639.21.185.136.324.328.397.55l.652 1.994c.118.361.41.635.77.728l2.957.752c.236.06.446.199.596.394.15.195.23.436.231.684v9.376c0 .248-.081.488-.231.684a1.09 1.09 0 01-.595.394l-2.957.752a1.086 1.086 0 00-.477.263 1.114 1.114 0 00-.293.465l-.653 1.994a1.11 1.11 0 01-.396.55c-.186.136-.41.21-.638.21h-1.702c-.229 0-.452-.073-.637-.21a1.11 1.11 0 01-.397-.55l-.364-1.11a1.131 1.131 0 01.15-1.002 1.074 1.074 0 01.885-.462h2.85c.29 0 .567-.116.772-.325.204-.208.32-.49.32-.785V6.444c0-.294-.116-.577-.32-.785a1.08 1.08 0 00-.771-.326h-3.273c-.29 0-.567.117-.772.326-.204.208-.32.49-.32.785v7.778c0 .295-.114.578-.319.786a1.08 1.08 0 01-.771.325h-2.182a1.08 1.08 0 01-.771-.325 1.122 1.122 0 01-.32-.786V6.444c0-.294-.115-.577-.32-.785a1.081 1.081 0 00-.77-.326H5.454c-.29 0-.567.117-.772.326-.204.208-.32.49-.32.785v11.112c0 .294.116.577.32.785.205.209.482.326.772.326h2.85a1.075 1.075 0 01.885.461 1.122 1.122 0 01.15 1.001l-.364 1.112a1.11 1.11 0 01-.397.55c-.185.136-.408.209-.637.209H6.24c-.229 0-.452-.073-.638-.21a1.11 1.11 0 01-.397-.55l-.652-1.994a1.114 1.114 0 00-.294-.465 1.086 1.086 0 00-.477-.263l-2.956-.752a1.09 1.09 0 01-.595-.394A1.124 1.124 0 010 16.688V7.312c0-.248.081-.489.231-.684.15-.195.36-.334.595-.394l2.957-.753c.178-.045.342-.136.477-.263.134-.127.235-.287.293-.464l.653-1.995a1.11 1.11 0 01.397-.55C5.788 2.075 6.01 2 6.24 2h1.701z" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,58 @@
|
||||
<script setup lang="ts">
|
||||
import { useFillIds } from '~/composables/useFillIds';
|
||||
|
||||
defineProps<{
|
||||
size?: string | number;
|
||||
color?: boolean;
|
||||
avatar?: boolean;
|
||||
}>();
|
||||
|
||||
const TITLE = 'Nova';
|
||||
|
||||
const [a, b, c] = useFillIds(TITLE, 3);
|
||||
|
||||
const BACKGROUND_COLOR = "linear-gradient(-45deg, #ff6200, #e433ff 39.9%, #6842ff 96%)";
|
||||
const AVATAR_SCALE = 0.7;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inline-flex items-center justify-center"
|
||||
:style="[`width: ${size}px; height: ${size}px;`, avatar ? `background: ${BACKGROUND_COLOR}; border-radius: 0.375rem;` : '']">
|
||||
<svg v-if="color && !avatar"
|
||||
:style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
viewBox="0 0 33 32" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<g :mask="a!.fill">
|
||||
<mask height="32" :id="a!.id" maskUnits="userSpaceOnUse" style="mask-type: luminance;" width="32" x="0"
|
||||
y="0">
|
||||
<path d="M31.8 0H0v32h31.8z" fill="#fff" />
|
||||
</mask>
|
||||
<mask height="32" :id="b!.id" maskUnits="userSpaceOnUse" style="mask-type: alpha;" width="32" x="0"
|
||||
y="0">
|
||||
<path
|
||||
d="m17.865 23.28 1.533 1.543c.07.07.092.175.055.267l-2.398 6.118A1.24 1.24 0 0 1 15.9 32c-.51 0-.969-.315-1.155-.793l-3.451-8.804-5.582 5.617a.246.246 0 0 1-.35 0l-1.407-1.415a.25.25 0 0 1 0-.352l6.89-6.932a1.3 1.3 0 0 1 .834-.398 1.25 1.25 0 0 1 1.232.79l2.992 7.63 1.557-3.977a.248.248 0 0 1 .408-.085zm8.224-19.3-5.583 5.617-3.45-8.805a1.24 1.24 0 0 0-1.43-.762c-.414.092-.744.407-.899.805l-2.38 6.072a.25.25 0 0 0 .055.267l1.533 1.543c.127.127.34.082.407-.085L15.9 4.655l2.991 7.629a1.24 1.24 0 0 0 2.035.425l6.922-6.965a.25.25 0 0 0 0-.352L26.44 3.977a.246.246 0 0 0-.35 0zM8.578 17.566l-3.953-1.567 7.582-3.01c.49-.195.815-.685.785-1.24a1.3 1.3 0 0 0-.395-.84l-6.886-6.93a.246.246 0 0 0-.35 0L3.954 5.395a.25.25 0 0 0 0 .353l5.583 5.617-8.75 3.472a1.25 1.25 0 0 0 0 2.325l6.079 2.412a.24.24 0 0 0 .266-.055l1.533-1.542a.25.25 0 0 0-.085-.41zm22.434-2.73-6.08-2.412a.24.24 0 0 0-.265.055l-1.533 1.542a.25.25 0 0 0 .084.41L27.172 16l-7.583 3.01a1.255 1.255 0 0 0-.785 1.24c.018.317.172.614.395.84l6.89 6.931a.246.246 0 0 0 .35 0l1.406-1.415a.25.25 0 0 0 0-.352l-5.582-5.617 8.75-3.472a1.25 1.25 0 0 0 0-2.325z"
|
||||
fill="#fff" />
|
||||
</mask>
|
||||
<g :mask="b!.fill">
|
||||
<path d="M-2.915 34.125h37.448V-2.109H-2.915z" :fill="c!.fill" />
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient gradientUnits="userSpaceOnUse" :id="c!.id" x1="33.663" x2="-2.086" y1="33.633"
|
||||
y2="-1.901">
|
||||
<stop stopColor="#ff6200" />
|
||||
<stop offset=".399" stopColor="#e433ff" />
|
||||
<stop offset=".96" stopColor="#6842ff" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
<svg v-else fill="currentColor" fill-rule="evenodd" :height="size" style="flex: none; line-height: 1;"
|
||||
:style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
viewBox="0 0 33 32" :width="size" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<path
|
||||
d="m17.865 23.28 1.533 1.543c.07.07.092.175.055.267l-2.398 6.118A1.24 1.24 0 0 1 15.9 32c-.51 0-.969-.315-1.155-.793l-3.451-8.804-5.582 5.617a.246.246 0 0 1-.35 0l-1.407-1.415a.25.25 0 0 1 0-.352l6.89-6.932a1.3 1.3 0 0 1 .834-.398 1.25 1.25 0 0 1 1.232.79l2.992 7.63 1.557-3.977a.248.248 0 0 1 .408-.085zm8.224-19.3-5.583 5.617-3.45-8.805a1.24 1.24 0 0 0-1.43-.762c-.414.092-.744.407-.899.805l-2.38 6.072a.25.25 0 0 0 .055.267l1.533 1.543c.127.127.34.082.407-.085L15.9 4.655l2.991 7.629a1.24 1.24 0 0 0 2.035.425l6.922-6.965a.25.25 0 0 0 0-.352L26.44 3.977a.246.246 0 0 0-.35 0zM8.578 17.566l-3.953-1.567 7.582-3.01c.49-.195.815-.685.785-1.24a1.3 1.3 0 0 0-.395-.84l-6.886-6.93a.246.246 0 0 0-.35 0L3.954 5.395a.25.25 0 0 0 0 .353l5.583 5.617-8.75 3.472a1.25 1.25 0 0 0 0 2.325l6.079 2.412a.24.24 0 0 0 .266-.055l1.533-1.542a.25.25 0 0 0-.085-.41zm22.434-2.73-6.08-2.412a.24.24 0 0 0-.265.055l-1.533 1.542a.25.25 0 0 0 .084.41L27.172 16l-7.583 3.01a1.255 1.255 0 0 0-.785 1.24c.018.317.172.614.395.84l6.89 6.931a.246.246 0 0 0 .35 0l1.406-1.415a.25.25 0 0 0 0-.352l-5.582-5.617 8.75-3.472a1.25 1.25 0 0 0 0-2.325z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
size?: string | number;
|
||||
color?: boolean;
|
||||
avatar?: boolean;
|
||||
}>();
|
||||
|
||||
const TITLE = 'Nvidia';
|
||||
|
||||
const BACKGROUND_COLOR = "#74B71B";
|
||||
const AVATAR_SCALE = 0.75;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inline-flex items-center justify-center"
|
||||
:style="[`width: ${size}px; height: ${size}px;`, avatar ? `background-color: ${BACKGROUND_COLOR}; border-radius: 0.375rem;` : '']">
|
||||
<svg :style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
:fill="color && !avatar ? BACKGROUND_COLOR : 'currentColor'"
|
||||
:fill-rule="color && !avatar ? 'evenodd' : 'nonzero'" style="flex: none; line-height: 1;"
|
||||
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<path
|
||||
d="M10.212 8.976V7.62c.127-.01.256-.017.388-.021 3.596-.117 5.957 3.184 5.957 3.184s-2.548 3.647-5.282 3.647a3.227 3.227 0 01-1.063-.175v-4.109c1.4.174 1.681.812 2.523 2.258l1.873-1.627a4.905 4.905 0 00-3.67-1.846 6.594 6.594 0 00-.729.044m0-4.476v2.025c.13-.01.259-.019.388-.024 5.002-.174 8.261 4.226 8.261 4.226s-3.743 4.69-7.643 4.69c-.338 0-.675-.031-1.007-.092v1.25c.278.038.558.057.838.057 3.629 0 6.253-1.91 8.794-4.169.421.347 2.146 1.193 2.501 1.564-2.416 2.083-8.048 3.763-11.24 3.763-.308 0-.603-.02-.894-.048V19.5H24v-15H10.21zm0 9.756v1.068c-3.356-.616-4.287-4.21-4.287-4.21a7.173 7.173 0 014.287-2.138v1.172h-.005a3.182 3.182 0 00-2.502 1.178s.615 2.276 2.507 2.931m-5.961-3.3c1.436-1.935 3.604-3.148 5.961-3.336V6.523C5.81 6.887 2 10.723 2 10.723s2.158 6.427 8.21 7.015v-1.166C5.77 16 4.25 10.958 4.25 10.958h-.002z" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,52 @@
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
size?: string | number;
|
||||
color?: boolean;
|
||||
avatar?: boolean;
|
||||
type?: string;
|
||||
}>();
|
||||
|
||||
const TITLE = 'OpenAI';
|
||||
|
||||
const COLOR_GPT_3 = '#19C37D';
|
||||
const COLOR_GPT_4 = '#AB68FF';
|
||||
const COLOR_GPT_5 = '#F86AA4';
|
||||
const COLOR_O_1 = '#F9C322';
|
||||
const COLOR_OSS = '#0099FF';
|
||||
const BACKGROUND_COLOR = "#0000FE";
|
||||
const AVATAR_SCALE = 0.7;
|
||||
|
||||
const background = computed(() => {
|
||||
switch (props.type) {
|
||||
case 'gpt3':
|
||||
return COLOR_GPT_3;
|
||||
case 'gpt4':
|
||||
return COLOR_GPT_4;
|
||||
case 'gpt5':
|
||||
return COLOR_GPT_5;
|
||||
case 'o3':
|
||||
case 'o1':
|
||||
return COLOR_O_1;
|
||||
case 'oss':
|
||||
return COLOR_OSS;
|
||||
case 'platform':
|
||||
return BACKGROUND_COLOR;
|
||||
default:
|
||||
return BACKGROUND_COLOR;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:style="[`width: ${size}px; height: ${size}px;`, avatar ? `background-color: ${background}; border-radius: 0.375rem;` : '']">
|
||||
<svg :style="[`width: ${size}px; height: ${size}px;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
fill="currentColor" fill-rule="evenodd" style="flex: none; line-height: 1;" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
|
||||
<path
|
||||
d="M9.205 8.658v-2.26c0-.19.072-.333.238-.428l4.543-2.616c.619-.357 1.356-.523 2.117-.523 2.854 0 4.662 2.212 4.662 4.566 0 .167 0 .357-.024.547l-4.71-2.759a.797.797 0 00-.856 0l-5.97 3.473zm10.609 8.8V12.06c0-.333-.143-.57-.429-.737l-5.97-3.473 1.95-1.118a.433.433 0 01.476 0l4.543 2.617c1.309.76 2.189 2.378 2.189 3.948 0 1.808-1.07 3.473-2.76 4.163zM7.802 12.703l-1.95-1.142c-.167-.095-.239-.238-.239-.428V5.899c0-2.545 1.95-4.472 4.591-4.472 1 0 1.927.333 2.712.928L8.23 5.067c-.285.166-.428.404-.428.737v6.898zM12 15.128l-2.795-1.57v-3.33L12 8.658l2.795 1.57v3.33L12 15.128zm1.796 7.23c-1 0-1.927-.332-2.712-.927l4.686-2.712c.285-.166.428-.404.428-.737v-6.898l1.974 1.142c.167.095.238.238.238.428v5.233c0 2.545-1.974 4.472-4.614 4.472zm-5.637-5.303l-4.544-2.617c-1.308-.761-2.188-2.378-2.188-3.948A4.482 4.482 0 014.21 6.327v5.423c0 .333.143.571.428.738l5.947 3.449-1.95 1.118a.432.432 0 01-.476 0zm-.262 3.9c-2.688 0-4.662-2.021-4.662-4.519 0-.19.024-.38.047-.57l4.686 2.71c.286.167.571.167.856 0l5.97-3.448v2.26c0 .19-.07.333-.237.428l-4.543 2.616c-.619.357-1.356.523-2.117.523zm5.899 2.83a5.947 5.947 0 005.827-4.756C22.287 18.339 24 15.84 24 13.296c0-1.665-.713-3.282-1.998-4.448.119-.5.19-.999.19-1.498 0-3.401-2.759-5.947-5.946-5.947-.642 0-1.26.095-1.88.31A5.962 5.962 0 0010.205 0a5.947 5.947 0 00-5.827 4.757C1.713 5.447 0 7.945 0 10.49c0 1.666.713 3.283 1.998 4.448-.119.5-.19 1-.19 1.499 0 3.401 2.759 5.946 5.946 5.946.642 0 1.26-.095 1.88-.309a5.96 5.96 0 004.162 1.713z" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
size?: string | number;
|
||||
color?: boolean;
|
||||
avatar?: boolean;
|
||||
}>();
|
||||
|
||||
const TITLE = 'OpenRouter';
|
||||
const AVATAR_SCALE = 0.7;
|
||||
|
||||
const BACKGROUND_COLOR = "#6566F1";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inline-flex items-center justify-center"
|
||||
:style="[`width: ${size}px; height: ${size}px;`, avatar ? `background-color: ${BACKGROUND_COLOR}; border-radius: 0.375rem;` : '']">
|
||||
<svg :style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
fill="currentColor" fill-rule="evenodd" style="flex: none; line-height: 1;" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<path
|
||||
d="M16.804 1.957l7.22 4.105v.087L16.73 10.21l.017-2.117-.821-.03c-1.059-.028-1.611.002-2.268.11-1.064.175-2.038.577-3.147 1.352L8.345 11.03c-.284.195-.495.336-.68.455l-.515.322-.397.234.385.23.53.338c.476.314 1.17.796 2.701 1.866 1.11.775 2.083 1.177 3.147 1.352l.3.045c.694.091 1.375.094 2.825.033l.022-2.159 7.22 4.105v.087L16.589 22l.014-1.862-.635.022c-1.386.042-2.137.002-3.138-.162-1.694-.28-3.26-.926-4.881-2.059l-2.158-1.5a21.997 21.997 0 00-.755-.498l-.467-.28a55.927 55.927 0 00-.76-.43C2.908 14.73.563 14.116 0 14.116V9.888l.14.004c.564-.007 2.91-.622 3.809-1.124l1.016-.58.438-.274c.428-.28 1.072-.726 2.686-1.853 1.621-1.133 3.186-1.78 4.881-2.059 1.152-.19 1.974-.213 3.814-.138l.02-1.907z" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,37 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
size?: string | number;
|
||||
color?: boolean;
|
||||
avatar?: boolean;
|
||||
}>();
|
||||
|
||||
const TITLE = 'PaLM';
|
||||
|
||||
const BACKGROUND_COLOR = "#FFF";
|
||||
const AVATAR_SCALE = 0.75;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inline-flex items-center justify-center"
|
||||
:style="[`width: ${size}px; height: ${size}px;`, avatar ? `background-color: ${BACKGROUND_COLOR}; border-radius: 0.375rem;` : '']">
|
||||
<svg :fill="avatar ? 'currentColor' : ''" fill-rule="evenodd" :height="size" style="flex: none; line-height: 1;"
|
||||
:style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
viewBox="0 0 24 24" :width="size" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<path :fill="color ? '#F9AB00' : ''"
|
||||
d="M12 22.926c.928 0 1.679-.752 1.679-1.68V6.696h-3.358v14.552c0 .927.751 1.679 1.679 1.679z" />
|
||||
<path :fill="color ? '#5BB974' : ''"
|
||||
d="M18.69 12.005A5.819 5.819 0 0012 10.904l7.188 7.188c.296.296.807.179.933-.22a5.815 5.815 0 00-1.431-5.867z" />
|
||||
<path :fill="color ? '#129EAF' : ''"
|
||||
d="M5.31 12.005A5.819 5.819 0 0112 10.904l-7.188 7.188a.562.562 0 01-.933-.22 5.815 5.815 0 011.431-5.867z" />
|
||||
<path :fill="color ? '#AF5CF7' : ''"
|
||||
d="M18.157 6.426c-2.86 0-5.288 1.875-6.157 4.478h11.367a.629.629 0 00.565-.908c-1.08-2.12-3.26-3.57-5.775-3.57z" />
|
||||
<path :fill="color ? '#FF8BCB' : ''"
|
||||
d="M13.188 3.384c-2.023 2.024-2.414 5.064-1.188 7.52l8.038-8.039a.629.629 0 00-.242-1.042c-2.264-.735-4.83-.217-6.608 1.561z" />
|
||||
<path :fill="color ? '#FA7B17' : ''"
|
||||
d="M10.812 3.384c2.023 2.024 2.414 5.064 1.188 7.52L3.962 2.865a.629.629 0 01.242-1.042c2.264-.735 4.83-.217 6.608 1.561z" />
|
||||
<path :fill="color ? '#4285F4' : ''"
|
||||
d="M5.843 6.426c2.86 0 5.288 1.875 6.157 4.478H.633a.629.629 0 01-.565-.908c1.08-2.12 3.26-3.57 5.775-3.57z" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
size?: string | number;
|
||||
color?: boolean;
|
||||
avatar?: boolean;
|
||||
}>();
|
||||
|
||||
const TITLE = 'Perplexity';
|
||||
|
||||
const BACKGROUND_COLOR = "#22B8CD";
|
||||
const AVATAR_SCALE = 0.75;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inline-flex items-center justify-center"
|
||||
:style="[`width: ${size}px; height: ${size}px;`, avatar ? `background-color: ${BACKGROUND_COLOR}; border-radius: 0.375rem;` : '']">
|
||||
<svg :style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
:fill="avatar ? '#000' : color ? BACKGROUND_COLOR : 'currentColor'"
|
||||
:fill-rule="color && !avatar ? 'evenodd' : 'nonzero'" style="flex: none; line-height: 1;"
|
||||
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<path
|
||||
d="M19.785 0v7.272H22.5V17.62h-2.935V24l-7.037-6.194v6.145h-1.091v-6.152L4.392 24v-6.465H1.5V7.188h2.884V0l7.053 6.494V.19h1.09v6.49L19.786 0zm-7.257 9.044v7.319l5.946 5.234V14.44l-5.946-5.397zm-1.099-.08l-5.946 5.398v7.235l5.946-5.234V8.965zm8.136 7.58h1.844V8.349H13.46l6.105 5.54v2.655zm-8.982-8.28H2.59v8.195h1.8v-2.576l6.192-5.62zM5.475 2.476v4.71h5.115l-5.115-4.71zm13.219 0l-5.115 4.71h5.115v-4.71z" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,39 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
size?: string | number;
|
||||
color?: boolean;
|
||||
avatar?: boolean;
|
||||
}>();
|
||||
|
||||
const TITLE = 'Qwen';
|
||||
const AVATAR_SCALE = 0.75;
|
||||
|
||||
const BACKGROUND_COLOR = "#615ced";
|
||||
const AVATAR_COLOR = '#fff';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inline-flex items-center justify-center"
|
||||
:style="[`width: ${size}px; height: ${size}px;`, avatar ? `background-color: ${BACKGROUND_COLOR}; border-radius: 0.375rem;` : '']">
|
||||
<svg v-if="color"
|
||||
:style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<path
|
||||
d="M12.604 1.34c.393.69.784 1.382 1.174 2.075a.18.18 0 00.157.091h5.552c.174 0 .322.11.446.327l1.454 2.57c.19.337.24.478.024.837-.26.43-.513.864-.76 1.3l-.367.658c-.106.196-.223.28-.04.512l2.652 4.637c.172.301.111.494-.043.77-.437.785-.882 1.564-1.335 2.34-.159.272-.352.375-.68.37-.777-.016-1.552-.01-2.327.016a.099.099 0 00-.081.05 575.097 575.097 0 01-2.705 4.74c-.169.293-.38.363-.725.364-.997.003-2.002.004-3.017.002a.537.537 0 01-.465-.271l-1.335-2.323a.09.09 0 00-.083-.049H4.982c-.285.03-.553-.001-.805-.092l-1.603-2.77a.543.543 0 01-.002-.54l1.207-2.12a.198.198 0 000-.197 550.951 550.951 0 01-1.875-3.272l-.79-1.395c-.16-.31-.173-.496.095-.965.465-.813.927-1.625 1.387-2.436.132-.234.304-.334.584-.335a338.3 338.3 0 012.589-.001.124.124 0 00.107-.063l2.806-4.895a.488.488 0 01.422-.246c.524-.001 1.053 0 1.583-.006L11.704 1c.341-.003.724.032.9.34zm-3.432.403a.06.06 0 00-.052.03L6.254 6.788a.157.157 0 01-.135.078H3.253c-.056 0-.07.025-.041.074l5.81 10.156c.025.042.013.062-.034.063l-2.795.015a.218.218 0 00-.2.116l-1.32 2.31c-.044.078-.021.118.068.118l5.716.008c.046 0 .08.02.104.061l1.403 2.454c.046.081.092.082.139 0l5.006-8.76.783-1.382a.055.055 0 01.096 0l1.424 2.53a.122.122 0 00.107.062l2.763-.02a.04.04 0 00.035-.02.041.041 0 000-.04l-2.9-5.086a.108.108 0 010-.113l.293-.507 1.12-1.977c.024-.041.012-.062-.035-.062H9.2c-.059 0-.073-.026-.043-.077l1.434-2.505a.107.107 0 000-.114L9.225 1.774a.06.06 0 00-.053-.031zm6.29 8.02c.046 0 .058.02.034.06l-.832 1.465-2.613 4.585a.056.056 0 01-.05.029.058.058 0 01-.05-.029L8.498 9.841c-.02-.034-.01-.052.028-.054l.216-.012 6.722-.012z"
|
||||
:fill="avatar ? AVATAR_COLOR : 'currentColor'" fill-rule="nonzero" />
|
||||
<defs>
|
||||
<linearGradient id={id} x1="0%" x2="100%" y1="0%" y2="0%">
|
||||
<stop offset="0%" stopColor="#6336E7" stopOpacity=".84" />
|
||||
<stop offset="100%" stopColor="#6F69F7" stopOpacity=".84" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
<svg v-else class="w-full h-full" fill="currentColor" fill-rule="evenodd" style="flex: none; line-height: 1;"
|
||||
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<path
|
||||
d="M12.604 1.34c.393.69.784 1.382 1.174 2.075a.18.18 0 00.157.091h5.552c.174 0 .322.11.446.327l1.454 2.57c.19.337.24.478.024.837-.26.43-.513.864-.76 1.3l-.367.658c-.106.196-.223.28-.04.512l2.652 4.637c.172.301.111.494-.043.77-.437.785-.882 1.564-1.335 2.34-.159.272-.352.375-.68.37-.777-.016-1.552-.01-2.327.016a.099.099 0 00-.081.05 575.097 575.097 0 01-2.705 4.74c-.169.293-.38.363-.725.364-.997.003-2.002.004-3.017.002a.537.537 0 01-.465-.271l-1.335-2.323a.09.09 0 00-.083-.049H4.982c-.285.03-.553-.001-.805-.092l-1.603-2.77a.543.543 0 01-.002-.54l1.207-2.12a.198.198 0 000-.197 550.951 550.951 0 01-1.875-3.272l-.79-1.395c-.16-.31-.173-.496.095-.965.465-.813.927-1.625 1.387-2.436.132-.234.304-.334.584-.335a338.3 338.3 0 012.589-.001.124.124 0 00.107-.063l2.806-4.895a.488.488 0 01.422-.246c.524-.001 1.053 0 1.583-.006L11.704 1c.341-.003.724.032.9.34zm-3.432.403a.06.06 0 00-.052.03L6.254 6.788a.157.157 0 01-.135.078H3.253c-.056 0-.07.025-.041.074l5.81 10.156c.025.042.013.062-.034.063l-2.795.015a.218.218 0 00-.2.116l-1.32 2.31c-.044.078-.021.118.068.118l5.716.008c.046 0 .08.02.104.061l1.403 2.454c.046.081.092.082.139 0l5.006-8.76.783-1.382a.055.055 0 01.096 0l1.424 2.53a.122.122 0 00.107.062l2.763-.02a.04.04 0 00.035-.02.041.041 0 000-.04l-2.9-5.086a.108.108 0 010-.113l.293-.507 1.12-1.977c.024-.041.012-.062-.035-.062H9.2c-.059 0-.073-.026-.043-.077l1.434-2.505a.107.107 0 000-.114L9.225 1.774a.06.06 0 00-.053-.031zm6.29 8.02c.046 0 .058.02.034.06l-.832 1.465-2.613 4.585a.056.056 0 01-.05.029.058.058 0 01-.05-.029L8.498 9.841c-.02-.034-.01-.052.028-.054l.216-.012 6.722-.012z" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
size?: string | number;
|
||||
color?: boolean;
|
||||
avatar?: boolean;
|
||||
}>();
|
||||
|
||||
const TITLE = 'Relace';
|
||||
const AVATAR_SCALE = 0.66;
|
||||
|
||||
const BACKGROUND_COLOR = "#000";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inline-flex items-center justify-center"
|
||||
:style="[`width: ${size}px; height: ${size}px;`, avatar ? `background-color: ${BACKGROUND_COLOR}; border-radius: 0.375rem;` : '']">
|
||||
<svg :style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
fill="currentColor" fill-rule="evenodd" style="flex: none; line-height: 1;" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<path
|
||||
d="M23 23H1V1h22v22zM2.962 15.232c1.969.3 5.028.42 7.78-.171.904-.195 1.743-.46 2.49-.803-1.395-3.4-1.675-5.766-1.264-7.378.466-1.823 1.799-2.59 2.998-2.602h.01c.688 0 2.117.177 3.081 1.35 1.003 1.22 1.216 3.152.26 5.991-.504 1.493-1.437 2.616-2.594 3.456 1.323 2.993 2.11 4.498 2.588 5.284.223.367.372.564.467.679h2.26V2.962H2.962v12.27zm11.05.827c-.912.413-1.887.711-2.857.92-2.91.626-6.059.527-8.193.234v3.825h13.471c-.527-.92-1.287-2.424-2.421-4.98zm.97-9.82c-.323.005-.87.181-1.112 1.127-.25.975-.161 2.776 1.055 5.84.705-.598 1.233-1.329 1.531-2.213.868-2.576.455-3.67.086-4.119-.406-.494-1.09-.633-1.56-.634z" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,42 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
size?: string | number;
|
||||
color?: boolean;
|
||||
avatar?: boolean;
|
||||
}>();
|
||||
|
||||
const TITLE = 'Stepfun';
|
||||
|
||||
const BACKGROUND_COLOR = "#005AFF";
|
||||
const AVATAR_SCALE = 0.6;
|
||||
|
||||
const [fill] = useFillIds(TITLE, 1);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inline-flex items-center justify-center"
|
||||
:style="[`width: ${size}px; height: ${size}px;`, avatar ? `background-color: ${BACKGROUND_COLOR}; border-radius: 0.375rem;` : '']">
|
||||
<svg v-if="color && !avatar"
|
||||
:style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<path
|
||||
d="M22.012 0h1.032v.927H24v.968h-.956V3.78h-1.032V1.896h-1.878v-.97h1.878V0zM2.6 12.371V1.87h.969v10.502h-.97zm10.423.66h10.95v.918h-6.208v9.579h-4.742V13.03zM5.629 3.333v12.356H0v4.51h10.386V8L20.859 8l-.003-4.668-15.227.001z"
|
||||
:fill="fill!.fill" fill-rule="evenodd" />
|
||||
<defs>
|
||||
<linearGradient gradientUnits="userSpaceOnUse" :id="fill!.id" x1="1.646" x2="18.342" y1="1.916"
|
||||
y2="22.091">
|
||||
<stop stop-color="#01A9FF" />
|
||||
<stop offset="1" stop-color="#0160FF" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
<svg v-else fill="currentColor" fill-rule="evenodd" :height="size" style="flex: none; line-height: 1;"
|
||||
:style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
viewBox="0 0 24 24" :width="size" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<path
|
||||
d="M22.012 0h1.032v.927H24v.968h-.956V3.78h-1.032V1.896h-1.878v-.97h1.878V0zM2.6 12.371V1.87h.969v10.502h-.97zm10.423.66h10.95v.918h-6.208v9.579h-4.742V13.03zM5.629 3.333v12.356H0v4.51h10.386V8L20.859 8l-.003-4.668-15.227.001z" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,50 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
size?: string | number;
|
||||
color?: boolean;
|
||||
avatar?: boolean;
|
||||
}>();
|
||||
|
||||
const TITLE = 'Upstage';
|
||||
|
||||
const BACKGROUND_COLOR = "linear-gradient(to bottom, #AEBCFE, #805DFA)";
|
||||
const AVATAR_SCALE = 0.6;
|
||||
|
||||
const [fill] = useFillIds(TITLE, 1);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inline-flex items-center justify-center"
|
||||
:style="[`width: ${size}px; height: ${size}px;`, avatar ? `background: ${BACKGROUND_COLOR}; border-radius: 0.375rem;` : '']">
|
||||
<svg v-if="color && !avatar"
|
||||
:style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<path
|
||||
d="M19.763 0l-.373 1.297h2.594L22.354 0h-2.591zM16.192 2.27l-.376 1.298h5.52l.37-1.298h-5.514zM12.897 4.54l-.376 1.298h8.166l.37-1.298h-8.16zM2.85 6.81l-.377 1.298h17.565l.37-1.297H2.848zM3.884 9.081l-.376 1.297H19.39l.37-1.297H3.882zM4.088 24l.376-1.297H1.866L1.5 24h2.588zM7.662 21.73l.376-1.297H2.515L2.15 21.73h5.513zM10.957 19.459l.376-1.297h-8.17l-.366 1.297h8.16zM21.005 17.189l.376-1.297H3.812l-.366 1.297h17.559zM19.967 14.919l.376-1.297H4.461l-.366 1.297h15.872zM18.786 12.649l.376-1.297H4.26l-.366 1.297h14.893z"
|
||||
:fill="fill!.fill" />
|
||||
<defs>
|
||||
<linearGradient gradientUnits="userSpaceOnUse" :id="fill!.id" x1="11.927" x2="11.927" y2="24">
|
||||
<stop offset="0" stop-color="#AEBCFE" />
|
||||
<stop offset="1" stop-color="#805DFA" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
<svg v-else fill="currentColor" fill-rule="evenodd" :height="size"
|
||||
:style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
viewBox="0 0 24 24" :width="size" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<path d="M19.763 0l-.373 1.297h2.594L22.354 0h-2.591z" />
|
||||
<path d="M16.192 2.27l-.376 1.298h5.52l.37-1.298h-5.514z" />
|
||||
<path d="M12.897 4.54l-.377 1.298h8.167l.37-1.297h-8.16z" />
|
||||
<path d="M2.85 6.81l-.377 1.298h17.565l.37-1.297H2.85z" />
|
||||
<path d="M3.884 9.081l-.376 1.297H19.39l.37-1.297H3.883z" />
|
||||
<path d="M4.088 24l.376-1.297H1.866L1.5 24h2.588z" />
|
||||
<path d="M7.662 21.73l.376-1.298H2.515L2.15 21.73h5.513z" />
|
||||
<path d="M10.957 19.46l.377-1.298h-8.17l-.367 1.297h8.16z" />
|
||||
<path d="M21.005 17.19l.376-1.298H3.812l-.366 1.297h17.559z" />
|
||||
<path d="M19.967 14.919l.376-1.297H4.461l-.366 1.297h15.872z" />
|
||||
<path d="M18.787 12.649l.376-1.298H4.26l-.366 1.298h14.893z" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,47 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
size?: string | number;
|
||||
color?: boolean;
|
||||
avatar?: boolean;
|
||||
}>();
|
||||
|
||||
const TITLE = 'VertexAI';
|
||||
|
||||
const BACKGROUND_COLOR = "#4285F4";
|
||||
const AVATAR_SCALE = 0.6;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inline-flex items-center justify-center"
|
||||
:style="[`width: ${size}px; height: ${size}px;`, avatar ? `background-color: ${BACKGROUND_COLOR}; border-radius: 0.375rem;` : '']">
|
||||
<svg :fill="avatar ? 'currentColor' : ''" fill-rule="evenodd" :height="size" style="flex: none; line-height: 1;"
|
||||
:style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
viewBox="0 0 24 24" :width="size" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<path
|
||||
d="M11.995 20.216a1.892 1.892 0 100 3.785 1.892 1.892 0 000-3.785zm0 2.806a.927.927 0 11.927-.914.914.914 0 01-.927.914z"
|
||||
:fill="!avatar && color ? '#4285F4' : ''" />
|
||||
<path clipRule="evenodd"
|
||||
d="M21.687 14.144c.237.038.452.16.605.344a.978.978 0 01-.18 1.3l-8.24 6.082a1.892 1.892 0 00-1.147-1.508l8.28-6.08a.991.991 0 01.682-.138z"
|
||||
:fill="!avatar && color ? '#669DF6' : ''" fillRule="evenodd" />
|
||||
<path clipRule="evenodd"
|
||||
d="M10.122 21.842l-8.217-6.066a.952.952 0 01-.206-1.287.978.978 0 011.287-.206l8.28 6.08a1.893 1.893 0 00-1.144 1.479z"
|
||||
:fill="!avatar && color ? '#AECBFA' : ''" fillRule="evenodd" />
|
||||
<path
|
||||
d="M4.273 4.475a.978.978 0 01-.965-.965V1.09a.978.978 0 111.943 0v2.42a.978.978 0 01-.978.965zM4.247 13.034a.978.978 0 100-1.956.978.978 0 000 1.956zM4.247 10.19a.978.978 0 100-1.956.978.978 0 000 1.956zM4.247 7.332a.978.978 0 100-1.956.978.978 0 000 1.956z"
|
||||
:fill="!avatar && color ? '#AECBFA' : ''" />
|
||||
<path
|
||||
d="M19.718 7.307a.978.978 0 01-.965-.979v-2.42a.965.965 0 011.93 0v2.42a.964.964 0 01-.965.979zM19.743 13.047a.978.978 0 100-1.956.978.978 0 000 1.956zM19.743 10.151a.978.978 0 100-1.956.978.978 0 000 1.956zM19.743 2.068a.978.978 0 100-1.956.978.978 0 000 1.956z"
|
||||
:fill="!avatar && color ? '#4285F4' : ''" />
|
||||
<path
|
||||
d="M11.995 15.917a.978.978 0 01-.965-.965v-2.459a.978.978 0 011.943 0v2.433a.976.976 0 01-.978.991zM11.995 18.762a.978.978 0 100-1.956.978.978 0 000 1.956zM11.995 10.64a.978.978 0 100-1.956.978.978 0 000 1.956zM11.995 7.783a.978.978 0 100-1.956.978.978 0 000 1.956z"
|
||||
:fill="!avatar && color ? '#669DF6' : ''" />
|
||||
<path
|
||||
d="M15.856 10.177a.978.978 0 01-.965-.965v-2.42a.977.977 0 011.702-.763.979.979 0 01.241.763v2.42a.978.978 0 01-.978.965zM15.869 4.913a.978.978 0 100-1.956.978.978 0 000 1.956zM15.869 15.853a.978.978 0 100-1.956.978.978 0 000 1.956zM15.869 12.996a.978.978 0 100-1.956.978.978 0 000 1.956z"
|
||||
:fill="!avatar && color ? '#4285F4' : ''" />
|
||||
<path
|
||||
d="M8.121 15.853a.978.978 0 100-1.956.978.978 0 000 1.956zM8.121 7.783a.978.978 0 100-1.956.978.978 0 000 1.956zM8.121 4.913a.978.978 0 100-1.957.978.978 0 000 1.957zM8.134 12.996a.978.978 0 01-.978-.94V9.611a.965.965 0 011.93 0v2.445a.966.966 0 01-.952.94z"
|
||||
:fill="!avatar && color ? '#AECBFA' : ''" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
size?: string | number;
|
||||
color?: boolean;
|
||||
avatar?: boolean;
|
||||
}>();
|
||||
|
||||
const TITLE = 'ChatGLM';
|
||||
|
||||
const BACKGROUND_COLOR = "linear-gradient(to right, #0A51C3, #23A4FB)";
|
||||
const AVATAR_SCALE = 0.75;
|
||||
|
||||
const [fill] = useFillIds(TITLE, 1);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inline-flex items-center justify-center"
|
||||
:style="[`width: ${size}px; height: ${size}px;`, avatar ? `background: ${BACKGROUND_COLOR}; border-radius: 0.375rem;` : '']">
|
||||
<svg v-if="color && !avatar"
|
||||
:style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<path
|
||||
d="M11.32 1.176a1.4 1.4 0 011.36 0l8.64 4.843c.421.234.68.67.68 1.141v9.68c0 .472-.259.908-.68 1.143l-8.64 4.84a1.4 1.4 0 01-1.36 0l-8.64-4.84A1.31 1.31 0 012 16.84V7.159c0-.471.259-.907.68-1.142l8.64-4.84zm7.42 13.839V8.227L12.002 12 12 19.551l6.059-3.394a1.31 1.31 0 00.68-1.142zM12.68 4.833a1.393 1.393 0 00-1.36 0L5.944 7.846c-.421.235-.68.67-.68 1.142v6.027c0 .47.259.905.68 1.142l2.795 1.566V11.09a1.546 1.546 0 00.221.79 1.527 1.527 0 01-.216-.834l.004-.094.02-.15.018-.084.017-.062.039-.117.062-.142.035-.065.081-.13.094-.122.084-.091.08-.075.125-.1.071-.048.134-.076 5.87-3.29-2.796-1.566z"
|
||||
:fill="fill!.fill" />
|
||||
<path
|
||||
d="M12 11.088c0-.875-.73-1.584-1.631-1.584a1.66 1.66 0 00-.855.237c-.027.016-.055.033-.08.05a2.361 2.361 0 00-.123.093c-.022.02-.045.038-.066.059l-.048.045-.063.067c-.014.016-.028.031-.04.048a2.303 2.303 0 00-.094.125l-.042.069a1.7 1.7 0 00-.07.13l-.036.081a.764.764 0 00-.022.06c-.01.03-.02.058-.028.087l-.017.062a.883.883 0 00-.03.16c-.002.025-.007.05-.008.074a1.527 1.527 0 00.213.929c.302.508.85.792 1.414.792.277 0 .558-.068.814-.212l.815-.457v-.914L12 11.088z"
|
||||
fill="#012F8D" />
|
||||
<defs>
|
||||
<linearGradient :id="fill!.id" x1="9.155%" x2="90.531%" y1="75.177%" y2="25.028%">
|
||||
<stop offset="0%" stop-color="#0A51C3" />
|
||||
<stop offset="100%" stop-color="#23A4FB" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
<svg v-else fill="currentColor" fill-rule="evenodd" :height="size" style="flex: none; line-height: 1;"
|
||||
:style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
viewBox="0 0 24 24" :width="size" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<path
|
||||
d="M11.32 1.176a1.4 1.4 0 011.36 0l8.64 4.843c.421.234.68.67.68 1.141v9.68c0 .472-.259.908-.68 1.143l-8.64 4.84a1.4 1.4 0 01-1.36 0l-8.64-4.84A1.31 1.31 0 012 16.84V7.159c0-.471.259-.907.68-1.142l8.64-4.84zm7.42 13.839V8.227L12.002 12 12 19.551l6.059-3.394a1.31 1.31 0 00.68-1.142zM12.68 4.833a1.393 1.393 0 00-1.36 0L5.944 7.846c-.421.235-.68.67-.68 1.142v6.027c0 .47.259.905.68 1.142l2.795 1.566V11.09a1.546 1.546 0 00.221.79 1.527 1.527 0 01-.216-.834l.004-.094.02-.15.018-.084.017-.062.039-.117.062-.142.035-.065.081-.13.094-.122.084-.091.08-.075.125-.1.071-.048.134-.076 5.87-3.29-2.796-1.566z" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
size?: string | number;
|
||||
color?: boolean;
|
||||
avatar?: boolean;
|
||||
}>();
|
||||
|
||||
const TITLE = 'XiaomiMiMo';
|
||||
const AVATAR_SCALE = 0.7;
|
||||
|
||||
const BACKGROUND_COLOR = "#000";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inline-flex items-center justify-center"
|
||||
:style="[`width: ${size}px; height: ${size}px;`, avatar ? `background-color: ${BACKGROUND_COLOR}; border-radius: 0.375rem;` : '']">
|
||||
<svg :style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
fill="currentColor" fill-rule="evenodd" style="flex: none; line-height: 1;" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<path
|
||||
d="M.958 15.936a.459.459 0 01.459.44v2.729a.46.46 0 01-.918 0v-2.729a.459.459 0 01.459-.44zm4.814-2.035a.46.46 0 01.553.45v4.754a.458.458 0 11-.918 0V15.48L3.74 17.202a.462.462 0 01-.655.016.462.462 0 01-.065-.082L.628 14.67a.459.459 0 01.658-.637l2.124 2.187 2.127-2.188a.46.46 0 01.235-.13zm2.068.004a.46.46 0 01.458.445v4.755a.46.46 0 01-.458.458.459.459 0 01-.458-.458V14.35a.459.459 0 01.458-.445zm1.973 2.014a.46.46 0 01.46.457v2.729a.46.46 0 01-.784.324.46.46 0 01-.134-.324v-2.729a.46.46 0 01.458-.458zm.002-2.045a.458.458 0 01.328.157l2.127 2.19 2.125-2.19a.459.459 0 01.784.318v4.756a.46.46 0 01-.455.458.46.46 0 01-.458-.458V15.48l-1.667 1.723a.46.46 0 01-.65.008l-.005-.005c0-.002-.002-.002-.004-.003l-2.455-2.534a.46.46 0 01-.008-.667.461.461 0 01.338-.128zm6.797 1.206a.46.46 0 01.53.651A1.966 1.966 0 0019.81 18.4a.462.462 0 01.623.18.46.46 0 01-.181.624 2.863 2.863 0 01-1.38.353l-.142-.004a2.88 2.88 0 01-2.393-4.263.461.461 0 01.274-.21zm.864-.931a2.884 2.884 0 013.915 3.914.46.46 0 01-.402.24l-.057-.004a.458.458 0 01-.164-.055.46.46 0 01-.182-.622 1.967 1.967 0 00-2.669-2.67.459.459 0 11-.441-.803zM9.59 6.368c1.481 0 1.696 1.202 1.696 1.654v2.648h-.917v-.432c-.26.346-.792.535-1.36.535-.133 0-1.289-.03-1.384-1.136-.082-.932.675-1.61 2.053-1.61h.691c0-.563-.367-.886-.983-.886-.44.013-.864.174-1.2.458l-.36-.664c.484-.379 1.012-.567 1.764-.567zm4.427.1c1.263 0 2.082.97 2.083 2.15 0 1.181-.824 2.154-2.083 2.154-1.26 0-2.084-.972-2.084-2.152 0-1.18.82-2.153 2.084-2.153zm6.801.015c.68 0 1.202.465 1.197 1.548v2.642H21.1V8.29c0-.312-.002-.98-.63-.98s-.628.667-.628.838v2.524h-.89V8.148c0-.17-.001-.838-.63-.838-.628 0-.628.668-.628.98v2.383h-.917v-4.03h.917V7a1.22 1.22 0 01.947-.516c.398 0 .76.193.982.686a1.321 1.321 0 011.195-.686zm-18.093.872l1.457-1.772H5.32L3.311 8.07l2.14 2.602H4.24L2.725 8.796 1.21 10.672H0L2.138 8.07.13 5.583h1.138l1.458 1.772zm4.149 3.317h-.916V6.644h.916v4.028zm16.99 0h-.916V6.644h.916v4.028zM9.925 8.71c-1.055 0-1.359.412-1.326.742.032.329.324.537.757.537a1.013 1.013 0 001.014-.968l.002-.31h-.447zM14.018 7.3c-.663 0-1.184.487-1.184 1.32 0 .832.52 1.32 1.184 1.32.662 0 1.182-.49 1.182-1.32 0-.832-.52-1.32-1.182-1.32zM6.417 5.001a.568.568 0 01.587.582.588.588 0 01-1.175 0A.57.57 0 016.417 5zm16.991 0a.57.57 0 01.592.582.588.588 0 01-1.174 0 .57.57 0 01.357-.542.572.572 0 01.225-.04z" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
size?: string | number;
|
||||
color?: boolean;
|
||||
avatar?: boolean;
|
||||
}>();
|
||||
|
||||
const TITLE = 'Z.ai';
|
||||
const AVATAR_SCALE = 0.6;
|
||||
|
||||
const BACKGROUND_COLOR = "#000";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inline-flex items-center justify-center"
|
||||
:style="[`width: ${size}px; height: ${size}px;`, avatar ? `background-color: ${BACKGROUND_COLOR}; border-radius: 0.375rem;` : '']">
|
||||
<svg :style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
|
||||
fill="currentColor" fill-rule="evenodd" style="flex: none; line-height: 1;" viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<title>{{ TITLE }}</title>
|
||||
<path
|
||||
d="M12.105 2L9.927 4.953H.653L2.83 2h9.276zM23.254 19.048L21.078 22h-9.242l2.174-2.952h9.244zM24 2L9.264 22H0L14.736 2H24z" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
@@ -19,6 +19,21 @@ const render = () => {
|
||||
}
|
||||
}
|
||||
|
||||
if (node.tagName === 'table') {
|
||||
return h('div', { class: 'table-wrapper' }, [
|
||||
h(
|
||||
'table',
|
||||
node.properties,
|
||||
node.children?.map((child: any, index: number) =>
|
||||
h(resolveComponent('MarkdownAstNode'), {
|
||||
node: child,
|
||||
key: `table-child-${index}`
|
||||
})
|
||||
)
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
return h(
|
||||
node.tagName,
|
||||
node.properties,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { RootContent } from 'hast';
|
||||
import MarkdownShikiHighlight from './ShikiHighlight.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
content: string;
|
||||
@@ -92,13 +93,14 @@ watch(parts, async (newParts) => {
|
||||
hastParts.value = stableBase.concat(latestHast.children);
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="prose-wrapper">
|
||||
<article class="markdown-body">
|
||||
<MarkdownAstNode v-for="(node, index) in hastParts" :key="`${id}-${index}`" :node="node" />
|
||||
<template v-for="(node, index) in hastParts" :key="`${id}-${index}`">
|
||||
<MarkdownAstNode :node="node" v-memo="[node.type === 'text' ? node.value : node.data]" />
|
||||
</template>
|
||||
</article>
|
||||
</div>
|
||||
</template>
|
||||
@@ -227,11 +229,16 @@ blockquote {
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
margin: calc(var(--spacing) * 4) 0;
|
||||
}
|
||||
|
||||
/* TODO: make these tables better, this is literally the first attempt from Gemini 3 flash */
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: calc(var(--spacing) * 4) 0;
|
||||
font-size: 0.95rem;
|
||||
text-align: left;
|
||||
background-color: var(--color-base);
|
||||
@@ -283,6 +290,11 @@ label>span {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin-top: 1.25rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
width: min-content;
|
||||
}
|
||||
|
||||
@@ -3,51 +3,138 @@ import { hashSync } from '~/utils/hash';
|
||||
const props = defineProps<{ code: string; lang: string }>();
|
||||
|
||||
const renderId = hashSync(props.code + props.lang);
|
||||
const codeBlockRef = ref<HTMLDivElement | null>(null);
|
||||
const codeHeight: Ref<string | number> = ref('auto');
|
||||
const copied = ref(false);
|
||||
const collapsed = ref(false);
|
||||
|
||||
const { data: html } = useAsyncData<string>(`shiki-${renderId}`, async () => parseCode());
|
||||
const { data: parsed } = useAsyncData(`shiki-${renderId}`, async () => {
|
||||
const { html, displayLang } = await parseCode();
|
||||
return { html, displayLang };
|
||||
});
|
||||
const lineNumberWidth = computed(() => {
|
||||
if (!html.value) return 1;
|
||||
if (!parsed.value?.html) return 1;
|
||||
// Count newlines in the generated HTML or the source code
|
||||
// Using props.code is safer and faster than parsing the HTML string
|
||||
return props.code.split('\n').length.toString().length;
|
||||
});
|
||||
|
||||
watch(() => props.code, async () => {
|
||||
html.value = await parseCode();
|
||||
|
||||
const { html: codeHtml } = await parseCode();
|
||||
parsed.value = { html: codeHtml, displayLang: parsed.value!.displayLang };
|
||||
});
|
||||
|
||||
async function parseCode() {
|
||||
const shiki = await getShikiHighlighter();
|
||||
let lang = props.lang.toLowerCase();
|
||||
let displayLang = lang;
|
||||
|
||||
try {
|
||||
shiki.getLanguage(lang);
|
||||
const shikiLang = shiki.getLanguage(lang);
|
||||
displayLang = shikiLang.name;
|
||||
} catch {
|
||||
lang = 'text';
|
||||
}
|
||||
return shiki.codeToHtml(props.code.trim(), {
|
||||
|
||||
const html = shiki.codeToHtml(props.code.trim(), {
|
||||
lang,
|
||||
themes: { dark: 'vitesse-dark', light: 'vitesse-light' },
|
||||
});
|
||||
|
||||
return { html, displayLang };
|
||||
}
|
||||
|
||||
let copyTimeout: NodeJS.Timeout | null = null;
|
||||
function copyCode() {
|
||||
copied.value = true;
|
||||
navigator.clipboard.writeText(props.code);
|
||||
|
||||
if (copyTimeout) clearTimeout(copyTimeout);
|
||||
copyTimeout = setTimeout(() => {
|
||||
copied.value = false;
|
||||
copyTimeout = null;
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
function collapseCode() {
|
||||
if (!codeBlockRef.value) return;
|
||||
|
||||
collapsed.value = !collapsed.value;
|
||||
if (collapsed.value) {
|
||||
codeHeight.value = codeBlockRef.value.scrollHeight;
|
||||
nextTick(() => {
|
||||
// since we are changing the height of an element, even though its
|
||||
// to its own height, we are triggering a reflow, which means that
|
||||
// if we didnt use requestAnimationFrame, the height would be set
|
||||
// to 0 before the reflow is complete, which would cause the reflow
|
||||
// to be ignored, and another one to be triggered with the new height
|
||||
// of zero, causing the codeblock to snap shut immediately rather than
|
||||
// animating. Not requestAnimationFrame because it doesnt work
|
||||
// on firefox, but setTimeout works on both chrome and firefox
|
||||
setTimeout(() => {
|
||||
codeHeight.value = 0;
|
||||
});
|
||||
});
|
||||
} else {
|
||||
codeBlockRef.value!.addEventListener('transitionend', () => {
|
||||
if (collapsed.value) return;
|
||||
codeHeight.value = 'auto';
|
||||
}, { once: true })
|
||||
|
||||
const targetHeight = codeBlockRef.value.scrollHeight;
|
||||
codeHeight.value = targetHeight;
|
||||
}
|
||||
}
|
||||
|
||||
const codeStyle = computed(() => {
|
||||
if (typeof codeHeight.value === 'number') {
|
||||
return `height: ${codeHeight.value}px;`;
|
||||
} else {
|
||||
return `height: ${codeHeight.value};`;
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (copyTimeout) clearTimeout(copyTimeout);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rounded-xl overflow-hidden code-container" :style="`--line-number-width: ${lineNumberWidth}ch`"
|
||||
:id="`code-${renderId}`" v-html="html">
|
||||
<div class="flex flex-col my-2 rounded-xl overflow-hidden">
|
||||
<div class="flex items-center pl-3 pr-1.5 py-1.5 text-sm font-sans bg-[var(--color-highlight)] justify-between">
|
||||
<div class="capitalize">
|
||||
{{ parsed?.displayLang }}
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button @click="copyCode()"
|
||||
class="flex items-center px-1 gap-0.5 rounded-md hover:bg-[var(--color-highlight)] transition-colors duration-150 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
|
||||
Copy
|
||||
<Icon v-if="!copied" name="mynaui:copy" class="text-4 text-[var(--color-text-subtle)]" />
|
||||
<Icon v-else name="mynaui:check" class="text-4 text-emerald-500" />
|
||||
</button>
|
||||
|
||||
<button @click="collapseCode()"
|
||||
class="flex items-center justify-center h-5.5 w-5.5 rounded-md hover:bg-[var(--color-highlight)] transition-colors duration-150 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
|
||||
<Icon name="mynaui:chevron-down"
|
||||
:class="['text-4 h-4 w-4 text-[var(--color-text-subtle)] transition-transform duration-150 ease-[cubic-bezier(0.5,_1,_0.89,_1)]', collapsed ? '-rotate-90' : '']" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="codeBlockRef"
|
||||
class="font-mono overflow-hidden code-container transition-height duration-300 ease-in-out"
|
||||
:style="`--line-number-width: ${lineNumberWidth}ch; ${codeStyle}`" :id="`code-${renderId}`"
|
||||
v-html="parsed?.html">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.code-container {
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.code-container>pre {
|
||||
overflow-x: auto;
|
||||
scrollbar-width: thin;
|
||||
padding: 1rem;
|
||||
line-height: 1.625;
|
||||
line-height: 0;
|
||||
counter-reset: lines;
|
||||
}
|
||||
|
||||
@@ -67,4 +154,11 @@ async function parseCode() {
|
||||
.light .code-container>pre>code .line::before {
|
||||
color: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.code-container>pre>code {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 0;
|
||||
font-variant-ligatures: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,12 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import type { Entity } from '@triplit/client';
|
||||
import type schema from '#triplit/schema';
|
||||
import ShikiHighlight from '~/components/Markdown/ShikiHighlight.vue';
|
||||
|
||||
defineProps<{
|
||||
error_part: Entity<typeof schema, 'message_parts'>;
|
||||
const props = defineProps<{
|
||||
error: string | null | undefined;
|
||||
}>();
|
||||
|
||||
const code: Ref<string | null> = ref(null);
|
||||
|
||||
if (props.error !== null && props.error !== undefined) {
|
||||
try {
|
||||
code.value = JSON.stringify(JSON.parse(props.error!), null, 2);
|
||||
} catch (e) {
|
||||
code.value = props.error;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<span class="text-sm text-[var(--color-error)]">Generation failed</span>
|
||||
<div class="text-sm">
|
||||
<ShikiHighlight :code="code ?? 'An unknown error occurred'" lang="json" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -9,7 +9,7 @@ const props = defineProps<{
|
||||
const activeTab = ref('input');
|
||||
|
||||
const indicatorStyle = computed(() => {
|
||||
const tabs = ['input', 'output', 'trace'];
|
||||
const tabs = ['input', 'output', 'error', 'trace'];
|
||||
const index = tabs.indexOf(activeTab.value);
|
||||
// Each tab button is ~48px (40px height + 8px gap)
|
||||
const offset = index * 48;
|
||||
@@ -27,19 +27,26 @@ const lineNumberWidth = ref(1);
|
||||
const input: ComputedRef<string> = computed(() => {
|
||||
switch (activeTab.value) {
|
||||
case 'input':
|
||||
if (props.toolCall.input === null) return '';
|
||||
if (props.toolCall.input === null || props.toolCall.input === undefined) return '';
|
||||
if (props.toolCall.input!.type === 'json') {
|
||||
return JSON.stringify(JSON.parse(props.toolCall.input!.value), null, 2);
|
||||
}
|
||||
|
||||
return props.toolCall.input!.value;
|
||||
case 'output':
|
||||
if (props.toolCall.output === null) return '';
|
||||
if (props.toolCall.output === null || props.toolCall.output === undefined) return '';
|
||||
if (props.toolCall.output!.type === 'json') {
|
||||
return JSON.stringify(JSON.parse(props.toolCall.output!.value), null, 2);
|
||||
}
|
||||
|
||||
return props.toolCall.output!.value;
|
||||
case 'error':
|
||||
if (props.toolCall.error === null || props.toolCall.error === undefined) return '';
|
||||
if (props.toolCall.error!.type === 'json') {
|
||||
return JSON.stringify(JSON.parse(props.toolCall.error!.value), null, 2);
|
||||
}
|
||||
|
||||
return props.toolCall.error!.value;
|
||||
case 'trace': {
|
||||
const traceObj: any = { ...props.toolCall };
|
||||
if (traceObj === null) return '';
|
||||
@@ -105,7 +112,7 @@ watch(
|
||||
<div class="w-full border rounded-lg border-[var(--color-highlight)] flex flex-row h-80 overflow-hidden">
|
||||
<div class="flex items-center gap-2 flex-col border-r border-[var(--color-highlight)] p-1 relative shrink-0">
|
||||
<button @click="activeTab = 'input'" :class="[
|
||||
'hover:bg-[var(--color-highlight)] p-2 rounded-lg flex gap-1 items-center w-full transition-colors duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]',
|
||||
'function-call-tab-selector',
|
||||
activeTab === 'input' ? 'text-orange-6' : ''
|
||||
]">
|
||||
<div
|
||||
@@ -114,18 +121,29 @@ watch(
|
||||
</div>
|
||||
Input
|
||||
</button>
|
||||
<button @click="activeTab = 'output'" :class="[
|
||||
'hover:bg-[var(--color-highlight)] p-2 rounded-lg flex gap-1 items-center w-full transition-colors duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]',
|
||||
activeTab === 'output' ? 'text-orange-6' : ''
|
||||
]">
|
||||
<button :disabled="toolCall.output === undefined || toolCall.output === null" @click="activeTab = 'output'"
|
||||
:class="[
|
||||
'function-call-tab-selector',
|
||||
activeTab === 'output' ? 'text-orange-6' : ''
|
||||
]">
|
||||
<div
|
||||
class="w-[24px] h-[24px] flex-shrink-0 rounded-lg overflow-hidden flex items-center justify-center">
|
||||
<Icon name="mynaui:arrow-down-square" class="w-4.5 h-4.5" />
|
||||
</div>
|
||||
Output
|
||||
</button>
|
||||
<button v-if="toolCall.error !== undefined && toolCall.error !== null" @click="activeTab = 'error'" :class="[
|
||||
'function-call-tab-selector',
|
||||
activeTab === 'error' ? 'text-orange-6' : ''
|
||||
]">
|
||||
<div
|
||||
class="w-[24px] h-[24px] flex-shrink-0 rounded-lg overflow-hidden flex items-center justify-center">
|
||||
<Icon name="mynaui:danger-triangle" class="w-4.5 h-4.5" />
|
||||
</div>
|
||||
Error
|
||||
</button>
|
||||
<button @click="activeTab = 'trace'" :class="[
|
||||
'hover:bg-[var(--color-highlight)] p-2 rounded-lg flex gap-1 items-center w-full transition-colors duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]',
|
||||
'function-call-tab-selector',
|
||||
activeTab === 'trace' ? 'text-orange-6' : ''
|
||||
]">
|
||||
<div
|
||||
@@ -174,4 +192,26 @@ watch(
|
||||
.light #function-call-container>pre>code .line::before {
|
||||
color: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.function-call-tab-selector {
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.375rem;
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
transition-property: color, background-color, border-color,
|
||||
text-decoration-color, fill, stroke;
|
||||
transition-duration: 200ms;
|
||||
transition-timing-function: cubic-bezier(0.5, 1, 0.89, 1);
|
||||
}
|
||||
|
||||
.function-call-tab-selector:hover:enabled {
|
||||
background-color: var(--color-highlight);
|
||||
}
|
||||
|
||||
.function-call-tab-selector:disabled {
|
||||
opacity: 0.3;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
@@ -12,28 +12,6 @@ const deubgToolCallOpen = ref(false);
|
||||
const toggleDebugToolCall = () => {
|
||||
deubgToolCallOpen.value = !deubgToolCallOpen.value;
|
||||
};
|
||||
|
||||
const iconName = ref('mynaui:tool');
|
||||
const iconColor = ref('var(--color-subtle)');
|
||||
|
||||
watch(
|
||||
() => props.toolCall.status,
|
||||
(status) => {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
iconName.value = 'svg-spinners:180-ring-with-bg';
|
||||
break;
|
||||
case 'completed':
|
||||
iconName.value = 'mynaui:tool';
|
||||
break;
|
||||
case 'failed':
|
||||
iconName.value = 'mynaui:x-solid';
|
||||
iconColor.value = '#ff3b3b';
|
||||
break;
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -44,8 +22,12 @@ watch(
|
||||
<div class="flex items-center gap-1">
|
||||
<div
|
||||
class="w-[24px] h-[24px] flex-shrink-0 rounded-lg overflow-hidden bg-[var(--color-neutral)] flex items-center justify-center">
|
||||
<Icon :name="iconName" :style="{ color: iconColor }"
|
||||
<!-- Explicityly avoid setting the name via a reactive value, otherwise you risk corrupting the icon with that name globally -->
|
||||
<Icon v-if="toolCall.status === 'pending'" name="svg-spinners:180-ring-with-bg"
|
||||
class="w-3 h-3 text-[var(--color-subtle)]" />
|
||||
<Icon v-else-if="toolCall.status === 'failed'" name="mynaui:x-solid"
|
||||
class="w-3 h-3 text-[#ff3b3b]" />
|
||||
<Icon v-else name="mynaui:tool" class="w-3 h-3 text-[var(--color-subtle)]" />
|
||||
</div>
|
||||
{{ toolCall.toolName }}
|
||||
</div>
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import type { Entity } from '@triplit/client';
|
||||
import type schema from '#triplit/schema';
|
||||
import ShikiHighlight from '~/components/Markdown/ShikiHighlight.vue';
|
||||
import Error from './Error.vue';
|
||||
import Reasoning from './Reasoning.vue';
|
||||
import Text from './Text.vue';
|
||||
import Tool from './Tool/index.vue';
|
||||
import type { MessageEntity } from '~/composables/useChat';
|
||||
|
||||
defineProps<{
|
||||
message: Readonly<
|
||||
Entity<typeof schema, 'messages'> & {
|
||||
parts: (Entity<typeof schema, 'message_parts'> & {
|
||||
toolCall: Entity<typeof schema, 'tool_calls'> | null;
|
||||
})[];
|
||||
} & { generation: Entity<typeof schema, 'generations'> | null }
|
||||
>;
|
||||
message: MessageEntity
|
||||
}>();
|
||||
</script>
|
||||
|
||||
@@ -28,28 +21,36 @@ defineProps<{
|
||||
Unhandled part type: {{ part.type }} {{ part }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="flex items-center gap-1"
|
||||
v-if="message.generation?.status === 'pending' && (message.parts || []).length === 0">
|
||||
<Icon name="svg-spinners:pulse-2" class="text-4" />
|
||||
<span class="text-sm text-[var(--color-muted)]">
|
||||
Preparing generating...
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<div v-else-if="message.generation?.status === 'failed'">
|
||||
<Error :error="message.generation.error" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row justify-between text-zinc-400 dark:text-zinc-600 text-xs"
|
||||
v-if="message.generation && message.generation.status !== 'pending'">
|
||||
<span class="flex items-center gap-1">
|
||||
<ModelIcon :size="12" :model-id="message.generation.modelId" />
|
||||
{{ message.generation.modelId }}
|
||||
</span>
|
||||
<span class="flex gap-1 items-center" v-if="message.generation.tokens?.output">
|
||||
<Icon name="tabler:coins" />
|
||||
{{ message.generation?.tokens?.output }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span v-if="message.generation?.status === 'pending' && message.parts.length === 0">
|
||||
<span class="text-sm text-[var(--color-muted)] flex flex-row items-center">
|
||||
<Icon name="svg-spinners:pulse-2" class="text-4" />
|
||||
Preparing generating...
|
||||
</span>
|
||||
</span>
|
||||
<div v-else-if="message.generation?.status === 'failed'">
|
||||
<span class="text-sm text-[var(--color-error)]">Generation failed</span>
|
||||
<div class="text-sm">
|
||||
<ShikiHighlight :code="message.generation.error ?? 'An unknown error occurred'" lang="json" />
|
||||
|
||||
<div class="flex gap-2">
|
||||
<span class="flex gap-1 items-center" v-if="message.generation.tokens?.output">
|
||||
<Icon name="tabler:coins" />
|
||||
{{ message.generation?.tokens?.output }}
|
||||
</span>
|
||||
|
||||
<span v-if="message.generation.tokens?.tps">
|
||||
{{ message.generation.tokens.tps.toFixed(1) }} tps
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,21 +1,149 @@
|
||||
<script setup lang="ts">
|
||||
import type { Entity } from '@triplit/client';
|
||||
import type schema from '#triplit/schema';
|
||||
import type { Message } from '~/composables/useChat';
|
||||
|
||||
defineProps<{
|
||||
message: Readonly<
|
||||
Entity<typeof schema, 'messages'> & {
|
||||
parts: (Entity<typeof schema, 'message_parts'> & {
|
||||
toolCall: Entity<typeof schema, 'tool_calls'> | null;
|
||||
})[];
|
||||
} & { generation: Entity<typeof schema, 'generations'> | null }
|
||||
>;
|
||||
const triplit = useTriplitClient();
|
||||
|
||||
const { message } = defineProps<{
|
||||
message: Message
|
||||
}>();
|
||||
|
||||
const focusedIndex = computed({
|
||||
get: () => {
|
||||
return message.focusedIndex || 0;
|
||||
},
|
||||
set: (newValue) => {
|
||||
triplit.update('messages', message.id, {
|
||||
focusedIndex: newValue
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
watch(() => message.children.length, (newCount, oldCount) => {
|
||||
if (newCount === oldCount) return;
|
||||
|
||||
// if we are deleting children, only move the focus index if its no longer valid
|
||||
// e.g. if we we have 4 children, and are focused on the 2nd, if we delete it,
|
||||
// we want to keep the focus on the 2nd index. It just makes me feel better
|
||||
if (oldCount > newCount) {
|
||||
if (message.deleted === true && focusedIndex.value === newCount) {
|
||||
focusedIndex.value = Math.max(0, newCount - 1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (focusedIndex.value > newCount) {
|
||||
|
||||
focusedIndex.value = newCount;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.deleted === true) {
|
||||
focusedIndex.value = newCount - 1;
|
||||
return;
|
||||
}
|
||||
|
||||
focusedIndex.value = newCount;
|
||||
});
|
||||
|
||||
const activeMessage = computed(() => {
|
||||
if (message.children.length === 0 || (focusedIndex.value === 0 && !message.deleted)) {
|
||||
return message;
|
||||
}
|
||||
|
||||
if (message.deleted === true) {
|
||||
return message.children[Math.min(focusedIndex.value, message.children.length - 1)];
|
||||
}
|
||||
|
||||
return message.children[focusedIndex.value - 1];
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
regenerate: []
|
||||
delete: []
|
||||
}>();
|
||||
|
||||
const copied = ref(false);
|
||||
const copyMessage = async () => {
|
||||
if (activeMessage.value!.role === 'user') {
|
||||
await navigator.clipboard.writeText(activeMessage.value!.content!);
|
||||
} else {
|
||||
let text = [];
|
||||
for (const part of activeMessage.value!.parts || []) {
|
||||
if (part.type === 'text') {
|
||||
text.push(part.content);
|
||||
}
|
||||
}
|
||||
|
||||
await navigator.clipboard.writeText(text.join('\n\n'));
|
||||
}
|
||||
|
||||
copied.value = true;
|
||||
setTimeout(() => {
|
||||
copied.value = false;
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
const regenerateMessage = async () => {
|
||||
emit('regenerate');
|
||||
}
|
||||
|
||||
const deleteMessage = () => {
|
||||
if (focusedIndex.value !== 0 && focusedIndex.value === message.children.length) {
|
||||
focusedIndex.value = Math.max(0, focusedIndex.value - 1);
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
emit('delete');
|
||||
});
|
||||
};
|
||||
|
||||
const messageCount = computed(() => {
|
||||
if (message.deleted === true) {
|
||||
return message.children.length;
|
||||
}
|
||||
|
||||
return message.children.length + 1;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="['max-w-full mb-4', message.role === 'user' ? 'pl-9 flex justify-end' : '']">
|
||||
<MessageUser v-if="message.role === 'user'" :message="message" />
|
||||
<MessageAgent v-else :message="message" />
|
||||
<div v-if="activeMessage" class="max-w-full mb-4 group flex flex-col">
|
||||
<div :class="[message.role === 'user' ? 'pl-9 flex justify-end' : '']">
|
||||
<MessageUser v-if="message.role === 'user'" :message="activeMessage!" />
|
||||
<MessageAgent v-else :message="activeMessage" />
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<div v-if="messageCount > 1" class="flex gap-1">
|
||||
<button :inert="focusedIndex === 0" @click="focusedIndex = focusedIndex! - 1">
|
||||
<Icon name="mynaui:chevron-left" :class="['w-4 h-4', focusedIndex === 0 ? 'opacity-0' : '']" />
|
||||
</button>
|
||||
<span class="text-xs text-[var(--color-muted)]">
|
||||
{{ focusedIndex + 1 }} / {{ messageCount }}
|
||||
</span>
|
||||
<button :inert="focusedIndex + 1 === messageCount" @click="focusedIndex = focusedIndex + 1">
|
||||
<Icon name="mynaui:chevron-right"
|
||||
:class="['w-4 h-4', focusedIndex + 1 === messageCount ? 'opacity-0' : '']" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :class="activeMessage.generation?.status === 'pending' ? 'opacity-0!' : ''"
|
||||
class="self-end mt-1 w-fit bg-[var(--color-highlight)] text-[var(--color-subtle)] gap-px flex items-center rounded-md overflow-hidden opacity-0 group-hover:opacity-100 transition-opacity duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
|
||||
<button @click="regenerateMessage"
|
||||
class="flex justify-center items-center w-7 h-6 hover:bg-[var(--color-highlight)]">
|
||||
<Icon name="mynaui:refresh" class="text-4.5" />
|
||||
</button>
|
||||
<button @click="copyMessage" :class="{ 'text-emerald-500': copied }"
|
||||
class="flex justify-center items-center w-7 h-6 hover:bg-[var(--color-highlight)]">
|
||||
<Icon :name="copied ? 'mynaui:check' : 'mynaui:copy'" class="text-4.5" />
|
||||
</button>
|
||||
<button @click="deleteMessage"
|
||||
class="flex justify-center items-center w-7 h-6 text-red-500 hover:bg-[var(--color-highlight)]">
|
||||
<Icon name="mynaui:trash" class="text-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -12,9 +12,13 @@ const config = computed(() => getModelConfig(props.modelId));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inline-flex items-center justify-center">
|
||||
<component :is="config.icon" v-if="config.icon" :avatar="avatar" :size="size" :color="variant === 'color'" />
|
||||
<div class="inline-flex items-center justify-center" v-bind="$attrs">
|
||||
<component v-if="config.Icon" :is="config.Icon" :avatar="avatar" :size="size" :color="variant === 'color'"
|
||||
v-bind="config.props" />
|
||||
<!-- Fallback if no logo matches -->
|
||||
<div v-else :style="{ width: `${props.size}px`, height: `${props.size}px` }" class="bg-gray-200 rounded-full" />
|
||||
<div v-else :style="{ width: `${props.size}px`, height: `${props.size}px` }"
|
||||
class="bg-[var(--color-highlight)] rounded-md flex items-center justify-center">
|
||||
<Icon name="tabler:brain" class="text-5 text-[var(--color-muted)]" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,136 @@
|
||||
<script setup lang="ts">
|
||||
import type { Entity } from '@triplit/client';
|
||||
import type schema from '#triplit/schema';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
model: Entity<typeof schema, 'models'>;
|
||||
size?: 'small' | 'normal';
|
||||
showCost?: boolean;
|
||||
showEdit?: boolean;
|
||||
showExternalId?: boolean;
|
||||
showReleaseDate?: boolean;
|
||||
}>(), {
|
||||
size: 'normal',
|
||||
showCost: false,
|
||||
showExternalId: false,
|
||||
showReleaseDate: false,
|
||||
showEdit: false,
|
||||
});
|
||||
|
||||
const { Big } = await import('big.js');
|
||||
|
||||
const triplit = useTriplitClient();
|
||||
|
||||
const formatContextWindow = (window: number | null | undefined): string => {
|
||||
if (!window) return '';
|
||||
if (window >= 1000000) return `${(window / 1000000).toFixed(0)}M`;
|
||||
if (window >= 1000) return `${(window / 1000).toFixed(0)}K`;
|
||||
return window.toString();
|
||||
};
|
||||
|
||||
const hasCapability = (capability: string): boolean => {
|
||||
return props.model.attributes.capabilities.has(capability);
|
||||
};
|
||||
|
||||
const hasInputModality = (modality: string): boolean => {
|
||||
return (props.model.attributes.inputModalities as Readonly<Set<string>>).has(modality);
|
||||
};
|
||||
|
||||
const formatBig = (bigValue: Big) => {
|
||||
let str = bigValue.toString();
|
||||
|
||||
if (!str.includes('.')) return str + '.00';
|
||||
if (str.split('.')[1]!.length === 1) return str + '0';
|
||||
|
||||
return str;
|
||||
};
|
||||
|
||||
const showCost = computed(() => {
|
||||
return props.showCost && (props.model.cost.prompt || props.model.cost.completion || props.model.cost.request);
|
||||
});
|
||||
|
||||
const toggleModel = async (id: string) => {
|
||||
await triplit.update('models', id, {
|
||||
enabled: !props.model.enabled,
|
||||
});
|
||||
};
|
||||
|
||||
const deleteModel = async () => {
|
||||
await triplit.delete('models', props.model.id);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center justify-between w-full" v-bind="$attrs">
|
||||
<div class="flex items-center gap-2 min-w-0 flex-1">
|
||||
<ModelIcon :class="size === 'normal' ? 'rounded-lg overflow-hidden' : ''" :avatar="true" variant="color"
|
||||
:model-id="model.externalId" :size="size === 'small' ? '20' : '32'" />
|
||||
|
||||
<div class="flex flex-col gap-1 min-w-0 flex-1">
|
||||
<div class="flex items-center gap-1 min-w-0">
|
||||
<span class="text-sm font-medium text-[var(--color-text)] truncate min-w-0">
|
||||
{{ model.name }}
|
||||
</span>
|
||||
<span v-if="showExternalId"
|
||||
class="text-xs text-[var(--color-muted)] px-1 py-0.5 rounded bg-[var(--color-highlight)] whitespace-nowrap truncate max-w-[240px]">
|
||||
{{ model.externalId }}
|
||||
</span>
|
||||
<div v-if="showEdit" class="flex items-center gap-2">
|
||||
<button @click="deleteModel"
|
||||
class="rounded h-5 w-5 flex items-center justify-center opacity-0 group-hover:opacity-100 hover:bg-red-500/30 text-red-500 trannsition-all duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
|
||||
<Icon name="mynaui:trash" class="text-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="size === 'normal'" class="flex items-center gap-1.5 flex-wrap">
|
||||
<span v-if="showReleaseDate && model.releasedAt"
|
||||
class="text-xs text-[var(--color-muted)] whitespace-nowrap">
|
||||
Released on {{ model.releasedAt.toISOString().split('T')[0] }}
|
||||
</span>
|
||||
<template v-if="showCost">
|
||||
<span v-if="model.cost.prompt"
|
||||
class="text-xs text-[var(--color-muted)] whitespace-nowrap flex items-center">
|
||||
<span class="w-1 h-1 rounded-full bg-[var(--color-muted)] inline-block mr-1"></span>
|
||||
${{ formatBig(Big(model.cost.prompt).mul(1000000)) }}/M input
|
||||
</span>
|
||||
<span v-if="model.cost.completion"
|
||||
class="text-xs text-[var(--color-muted)] whitespace-nowrap flex items-center">
|
||||
<span class="w-1 h-1 rounded-full bg-[var(--color-muted)] inline-block mr-1"></span>
|
||||
${{ formatBig(Big(model.cost.completion).mul(1000000)) }}/M output
|
||||
</span>
|
||||
<span v-if="model.cost.request && model.cost.request !== '0'"
|
||||
class="text-xs text-[var(--color-muted)] whitespace-nowrap flex items-center">
|
||||
<span class="w-1 h-1 rounded-full bg-[var(--color-muted)] inline-block mr-1"></span>
|
||||
{{ model.cost.request }}/request
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-1 shrink-0">
|
||||
<div class="flex items-center gap-0.5">
|
||||
<div v-if="hasInputModality('image')"
|
||||
class="w-4.5 h-4.5 bg-emerald/10 rounded flex items-center justify-center">
|
||||
<Icon name="mynaui:image" class="text-2.5 text-emerald" title="Vision" />
|
||||
</div>
|
||||
<div v-if="hasCapability('reasoning')"
|
||||
class="w-4.5 h-4.5 bg-[color-mix(in_srgb,_transparent_90%,_var(--reasoning-accent)_10%)] rounded flex items-center justify-center">
|
||||
<Icon name="mynaui:atom" class="text-2.5 text-[var(--reasoning-accent)]" title="Reasoning" />
|
||||
</div>
|
||||
<div v-if="hasCapability('tools')"
|
||||
class="w-4.5 h-4.5 bg-emerald/10 rounded flex items-center justify-center">
|
||||
<Icon name="mynaui:tool" class="text-2.5 text-sky" title="Tools" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span v-if="model.attributes.contextWindow"
|
||||
class="text-xs font-mono text-[var(--color-subtle)] px-1.5 py-0.5 rounded bg-[var(--color-highlight)]">
|
||||
{{ formatContextWindow(model.attributes.contextWindow) }}
|
||||
</span>
|
||||
|
||||
<Slider v-if="showEdit" :checked="model.enabled" @click="toggleModel(model.id)" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,79 +1,111 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted, onUnmounted } from 'vue';
|
||||
import type { ModelWithProvider, ProviderWithModels } from '~/composables/useModels';
|
||||
import { schema } from '#triplit/schema';
|
||||
import type { Entity } from '@triplit/client';
|
||||
import type schema from '#triplit/schema';
|
||||
|
||||
const { setPage } = useSettings();
|
||||
const { allModels } = await useModels();
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: ModelWithProvider | null;
|
||||
providers: ProviderWithModels[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [model: ModelWithProvider | null];
|
||||
}>();
|
||||
|
||||
const isOpen = ref(false);
|
||||
const searchQuery = ref('');
|
||||
const dropdownRef = ref<HTMLDivElement | null>(null);
|
||||
const searchInputRef = ref<HTMLInputElement | null>(null);
|
||||
const selectedModel = defineModel<ModelWithProvider | null>();
|
||||
|
||||
const selectedModel = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value),
|
||||
});
|
||||
const dropdownRef = ref<HTMLDivElement | null>(null);
|
||||
const dropdownButton = ref<HTMLButtonElement | null>(null);
|
||||
const searchInputRef = ref<HTMLInputElement | null>(null);
|
||||
const dropdownDirection = ref<'up' | 'down'>('up');
|
||||
const dropdownMaxHeight = ref<number | undefined>(undefined);
|
||||
|
||||
const findContainer = (startingElement: HTMLElement): HTMLElement | null => {
|
||||
let container: HTMLElement | null = startingElement;
|
||||
while (container) {
|
||||
const computedStyle = getComputedStyle(container);
|
||||
if (computedStyle.overflow !== 'visible') {
|
||||
return container;
|
||||
}
|
||||
container = container.parentElement;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const calculateDropdownPosition = () => {
|
||||
if (!dropdownButton.value) return;
|
||||
|
||||
const buttonRect = dropdownButton.value.getBoundingClientRect();
|
||||
const container = findContainer(dropdownButton.value) || document.body;
|
||||
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
|
||||
const spaceAbove = buttonRect.top - containerRect.top;
|
||||
const spaceBelow = containerRect.bottom - buttonRect.bottom;
|
||||
|
||||
if (spaceAbove > spaceBelow) {
|
||||
dropdownDirection.value = 'up';
|
||||
dropdownMaxHeight.value = Math.min(spaceAbove - 20, 460);
|
||||
} else {
|
||||
dropdownDirection.value = 'down';
|
||||
dropdownMaxHeight.value = Math.min(spaceBelow - 20, 460);
|
||||
}
|
||||
};
|
||||
|
||||
const searchQuery = ref('');
|
||||
|
||||
const filteredProviders = computed(() => {
|
||||
if (!searchQuery.value.trim()) {
|
||||
return props.providers;
|
||||
}
|
||||
|
||||
const query = searchQuery.value.toLowerCase();
|
||||
return props.providers
|
||||
.map((provider) => ({
|
||||
...provider,
|
||||
models: provider.models.filter((model) =>
|
||||
model.name.toLowerCase().includes(query)
|
||||
),
|
||||
}))
|
||||
.filter((provider) => provider.models.length > 0);
|
||||
return filterProvidersWithModel(props.providers, searchQuery.value).filter(p => p.enabled);
|
||||
});
|
||||
|
||||
const formatContextWindow = (window: number | null | undefined): string => {
|
||||
if (!window) return '';
|
||||
if (window >= 1000000) return `${(window / 1000000).toFixed(0)}M`;
|
||||
if (window >= 1000) return `${(window / 1000).toFixed(0)}K`;
|
||||
return window.toString();
|
||||
};
|
||||
|
||||
const hasCapability = (model: Entity<typeof schema, 'models'>, capability: string): boolean => {
|
||||
return model.attributes.capabilities.has(capability);
|
||||
};
|
||||
|
||||
const hasInputModality = (model: Entity<typeof schema, 'models'>, modality: string): boolean => {
|
||||
return model.attributes.inputModalities.has(modality);
|
||||
};
|
||||
|
||||
const selectModel = (model: Entity<typeof schema, 'models'>, provider: Entity<typeof schema, 'providers'>) => {
|
||||
selectedModel.value = { ...model, provider };
|
||||
isOpen.value = false;
|
||||
searchQuery.value = '';
|
||||
};
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
isOpen.value = false;
|
||||
watch(() => props.providers, () => {
|
||||
if (selectedModel.value) {
|
||||
const modelStillExists = allModels.value.some(model => model.id === selectedModel.value?.id);
|
||||
|
||||
if (!modelStillExists) {
|
||||
let firstEnabled: ModelWithProvider | null = null;
|
||||
|
||||
for (const provider of props.providers) {
|
||||
const enabledModel = provider.models.find(m => m.enabled);
|
||||
if (enabledModel) {
|
||||
firstEnabled = { ...enabledModel, provider };
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
selectedModel.value = firstEnabled;
|
||||
}
|
||||
}
|
||||
};
|
||||
})
|
||||
|
||||
watch(isOpen, (open) => {
|
||||
if (open) {
|
||||
calculateDropdownPosition();
|
||||
nextTick(() => {
|
||||
searchInputRef.value?.focus();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const handleInputKeypress = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Enter') {
|
||||
isOpen.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
// TODO: in the settings page, pressing escape closes BOTH
|
||||
// the settings modal and the model selector dropdown
|
||||
if (event.key === 'Escape') {
|
||||
isOpen.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
useClickOutside(dropdownRef, () => {
|
||||
isOpen.value = false;
|
||||
});
|
||||
@@ -89,16 +121,15 @@ onUnmounted(() => {
|
||||
|
||||
<template>
|
||||
<div ref="dropdownRef" class="relative">
|
||||
<button @click="isOpen = !isOpen"
|
||||
<button @click="isOpen = !isOpen" ref="dropdownButton"
|
||||
class="flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm font-medium transition-colors duration-200"
|
||||
:class="[
|
||||
isOpen
|
||||
? 'bg-[var(--color-highlight)] text-[var(--color-text)]'
|
||||
: 'text-[var(--color-text-subtle)] hover:text-[var(--color-text)] hover:bg-[var(--color-highlight-low)]',
|
||||
]">
|
||||
<ModelIcon v-if="selectedModel" :avatar="true" variant="color" :model-id="selectedModel.externalId"
|
||||
size="16" />
|
||||
<Icon v-else name="mynaui:warning-circle" class="text-4" />
|
||||
<ModelIcon v-if="selectedModel" class="text-white" :avatar="true" variant="color"
|
||||
:model-id="selectedModel.externalId" size="22" />
|
||||
<span class="max-w-[150px] truncate">
|
||||
{{ selectedModel ? selectedModel.name : 'Select a model' }}
|
||||
</span>
|
||||
@@ -106,74 +137,59 @@ onUnmounted(() => {
|
||||
:class="{ 'rotate-180': isOpen }" />
|
||||
</button>
|
||||
|
||||
|
||||
<Transition enter-active-class="transition-all duration-150 ease-[cubic-bezier(0.5,_1,_0.89,_1)]"
|
||||
enter-from-class="opacity-0 scale-95 translate-y-1" enter-to-class="opacity-100 scale-100 translate-y-0"
|
||||
leave-active-class="transition-all duration-100 ease-[cubic-bezier(0.5,_1,_0.89,_1)]"
|
||||
leave-from-class="opacity-100 scale-100 translate-y-0" leave-to-class="opacity-0 scale-95 translate-y-1">
|
||||
<div v-if="isOpen"
|
||||
class="transform-origin-bottom-center absolute bottom-full left-0 mb-2 max-w-[420px] w-full max-h-[460px] flex flex-col rounded-xl border border-[var(--color-highlight)] bg-[var(--color-neutral)] shadow-lg overflow-hidden z-50">
|
||||
<div v-show="isOpen" ref="dropdownContentRef" :class="[
|
||||
dropdownDirection === 'up' ? 'bottom-full mb-2 origin-bottom' : 'top-full mt-2 origin-top',
|
||||
]" :style="{ maxHeight: dropdownMaxHeight ? `${dropdownMaxHeight}px` : '460px', height: 'auto' }"
|
||||
class="absolute left-0 max-w-[420px] w-full flex flex-col rounded-xl border border-[var(--color-highlight)] bg-[var(--color-neutral)] shadow-lg overflow-hidden z-50">
|
||||
<div>
|
||||
<div class="relative">
|
||||
<Icon name="mynaui:search"
|
||||
class="absolute left-3 top-1/2 -translate-y-1/2 text-4 text-[var(--color-text-subtle)]" />
|
||||
<input ref="searchInputRef" v-model="searchQuery" type="text" placeholder="Search models..."
|
||||
<input ref="searchInputRef" v-model="searchQuery" autocomplete="off" name="search"
|
||||
@keypress="handleInputKeypress" type="text" placeholder="Search models..."
|
||||
class="w-full pl-9 pr-3 py-2 text-sm text-[var(--color-text)] bg-transparent placeholder-[var(--color-text-subtle)] outline-none" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto py-2 select-none">
|
||||
<div class="flex-1 overflow-y-auto [scrollbar-width:thin] py-2 select-none max-w-full overflow-hidden">
|
||||
<div v-if="filteredProviders.length === 0"
|
||||
class="px-4 py-8 text-center text-sm text-[var(--color-muted)]">
|
||||
No models found
|
||||
</div>
|
||||
|
||||
<div v-for="provider in filteredProviders" :key="provider.id" class="mb-2">
|
||||
<div class="px-4 py-1.5 text-xs font-medium text-[var(--color-muted)] uppercase tracking-wider">
|
||||
<div
|
||||
class="px-4 py-1.5 text-xs font-medium text-[var(--color-muted)] capitalize tracking-wider flex justify-between">
|
||||
{{ provider.name }}
|
||||
<button @click="isOpen = true; setPage('providers', provider.id)"
|
||||
class="flex h-4.5 w-4.5 items-center justify-center hover:bg-[var(--color-highlight)] rounded transition-colors duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
|
||||
<Icon name="mynaui:cog-four" class="text-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button v-for="model in provider.models.filter(m => m.enabled)" :key="model.id"
|
||||
@click="selectModel(model, provider)"
|
||||
class="w-full px-4 py-2 flex items-center gap-3 hover:bg-[var(--color-highlight-low)] transition-colors duration-150"
|
||||
<button
|
||||
v-for="model in provider.models.filter(m => m.enabled).sort((a, b) => b.releasedAt && a.releasedAt ? b.releasedAt.getTime() - a.releasedAt.getTime() : 0)"
|
||||
:key="model.id" @click="selectModel(model, provider); isOpen = false"
|
||||
class="text-white w-full min-h-9 px-4 py-2 flex items-center justify-between hover:bg-[var(--color-highlight-low)] transition-colors duration-150"
|
||||
:class="{ 'bg-[var(--color-highlight-low)]': selectedModel?.id === model.id }">
|
||||
<ModelIcon :avatar="true" variant="color" :model-id="model.externalId" size="20" />
|
||||
|
||||
<span class="flex-1 text-sm text-left text-[var(--color-text)] truncate">
|
||||
{{ model.name }}
|
||||
</span>
|
||||
|
||||
<div class="flex items-center gap-0.5">
|
||||
<div v-if="hasInputModality(model, 'image')"
|
||||
class="w-4.5 h-4.5 bg-emerald/10 rounded flex items-center justify-center">
|
||||
<Icon name="mynaui:image" class="text-2.5 text-emerald" title="Vision" />
|
||||
</div>
|
||||
<div v-if="hasCapability(model, 'reasoning')"
|
||||
class="w-4.5 h-4.5 bg-[color-mix(in_srgb,_transparent_90%,_var(--reasoning-accent)_10%)] rounded flex items-center justify-center">
|
||||
<Icon name="mynaui:atom" class="text-2.5 text-[var(--reasoning-accent)]"
|
||||
title="Reasoning" />
|
||||
</div>
|
||||
<div v-if="hasCapability(model, 'tools')"
|
||||
class="w-4.5 h-4.5 bg-emerald/10 rounded flex items-center justify-center">
|
||||
<Icon name="mynaui:tool" class="text-2.5 text-sky" title="Tools" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span v-if="model.attributes.contextWindow"
|
||||
class="text-xs font-mono text-[var(--color-subtle)] px-1.5 py-0.5 rounded bg-[var(--color-highlight)]">
|
||||
{{ formatContextWindow(model.attributes.contextWindow) }}
|
||||
</span>
|
||||
<ModelInfo :model="model" size="small" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-1 border-t border-[var(--color-highlight-low)]">
|
||||
<NuxtLink to="/settings/providers"
|
||||
class="flex items-center gap-2 px-3 py-2 text-sm text-[var(--color-text-subtle)] hover:text-[var(--color-text)] hover:bg-[var(--color-highlight-low)] rounded-lg transition-colors duration-150"
|
||||
@click="isOpen = false">
|
||||
<button
|
||||
class="flex w-full items-center gap-2 px-3 py-2 text-sm text-[var(--color-text-subtle)] hover:text-[var(--color-text)] hover:bg-[var(--color-highlight-low)] rounded-lg transition-colors duration-150"
|
||||
@click="isOpen = false; setPage('providers');">
|
||||
<Icon name="mynaui:cog-four" class="text-4" />
|
||||
<span>Manage Provider</span>
|
||||
<span>Manage Providers</span>
|
||||
<Icon name="mynaui:arrow-right" class="text-3.5 ml-auto" />
|
||||
</NuxtLink>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { encryptData, decrypt, uint8ArrayToBase64, base64ToUint8Array } from '~/utils/crypto';
|
||||
import { providerBaseUrls } from '~/types/model';
|
||||
import { providerBaseUrls, SupportedModalities, type Model } from '~/types/model';
|
||||
import { useSettings } from '~/composables/useSettings';
|
||||
import ModelItem from './ModelItem.vue';
|
||||
// @ts-ignore
|
||||
import { DynamicScroller, DynamicScrollerItem } from 'vue-virtual-scroller'
|
||||
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
|
||||
const triplit = useTriplitClient();
|
||||
|
||||
const { pageParams } = useSettings();
|
||||
|
||||
const { providers } = await useModels();
|
||||
const { providers, unsubscribe: unsubscribeModels } = await useModels();
|
||||
|
||||
const provider = computed(() => {
|
||||
if (pageParams.value.length === 0) return null;
|
||||
@@ -21,12 +24,22 @@ watch(provider, async () => {
|
||||
const apiKeyVisible = ref(false);
|
||||
|
||||
const apiKey = ref('');
|
||||
const apiProxyUrl = ref(provider.value!.config.apiProxyUrl ?? '');
|
||||
const apiProxyUrl = ref(provider.value?.config.apiProxyUrl ?? '');
|
||||
const modelSearch = ref('');
|
||||
|
||||
const providerApiUrl = computed(() => apiProxyUrl.value === '' ? providerBaseUrls[provider.value!.type] : apiProxyUrl.value);
|
||||
watch(pageParams, () => {
|
||||
if (pageParams.value.length === 0) return;
|
||||
apiKey.value = provider.value?.config.apiKey ?? '';
|
||||
apiProxyUrl.value = provider.value?.config.apiProxyUrl ?? '';
|
||||
modelSearch.value = '';
|
||||
})
|
||||
|
||||
const decryptApiKey = async () => {
|
||||
if (!provider.value?.config.apiKey) {
|
||||
apiKey.value = '';
|
||||
return
|
||||
};
|
||||
|
||||
const key = await crypto.subtle.importKey(
|
||||
"jwk",
|
||||
JSON.parse(window.localStorage.getItem("encryptionKey")!),
|
||||
@@ -70,6 +83,8 @@ const updateApiKey = async (value: string) => {
|
||||
const updateProxyUrl = async (value: string) => {
|
||||
if (!provider.value) return;
|
||||
|
||||
apiProxyUrl.value = value;
|
||||
|
||||
await triplit.update('providers', provider.value.id, {
|
||||
config: {
|
||||
...provider.value.config,
|
||||
@@ -78,14 +93,6 @@ const updateProxyUrl = async (value: string) => {
|
||||
});
|
||||
};
|
||||
|
||||
const toggleModel = async (id: string) => {
|
||||
if (!provider.value) return;
|
||||
|
||||
await triplit.update('models', id, {
|
||||
enabled: !provider.value!.models.find(m => m.id === id)!.enabled,
|
||||
});
|
||||
};
|
||||
|
||||
const fetchingModels = ref(false);
|
||||
|
||||
const fetchModels = async () => {
|
||||
@@ -95,12 +102,23 @@ const fetchModels = async () => {
|
||||
|
||||
try {
|
||||
const [providerResponse, devDataResponse] = await Promise.all([
|
||||
$fetch(`${providerApiUrl.value}/models`),
|
||||
$fetch('https://models.dev/api.json')
|
||||
$fetch(`/api/provider/${provider.value!.id}/models`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
providerApiKey: apiKey.value
|
||||
})
|
||||
}) as any,
|
||||
$fetch('https://models.dev/api.json') as any
|
||||
]);
|
||||
|
||||
const providerType = provider.value!.type;
|
||||
// TODO: get model details correctly for ollama-cloud models
|
||||
let providerType = provider.value!.type as string;
|
||||
if (providerType === 'ollama') {
|
||||
providerType = 'ollama-cloud';
|
||||
}
|
||||
|
||||
const modelDetails = devDataResponse[providerType]?.models || {};
|
||||
console.log(modelDetails);
|
||||
|
||||
const existingModelsMap = new Map(
|
||||
(provider.value?.models || []).map((m: any) => [m.externalId, m])
|
||||
@@ -109,11 +127,15 @@ const fetchModels = async () => {
|
||||
const toInsert: any[] = [];
|
||||
const toUpdate: { id: string, data: any }[] = [];
|
||||
|
||||
providerResponse.data.forEach((pModel: any) => {
|
||||
const slug = pModel.id.toLowerCase();
|
||||
const info = modelDetails[slug] || {};
|
||||
providerResponse.models.forEach((pModel: any) => {
|
||||
let slug: string = pModel.id.toLowerCase();
|
||||
if (providerType === 'ollama-cloud') {
|
||||
slug = slug.replace(/:cloud$/, '');
|
||||
slug = slug.replace(/-cloud$/, '');
|
||||
slug = slug.replace(/:latest$/, '');
|
||||
}
|
||||
|
||||
console.log("INFO", info);
|
||||
let info = modelDetails[slug] || {};
|
||||
|
||||
const capabilities = [];
|
||||
|
||||
@@ -125,14 +147,48 @@ const fetchModels = async () => {
|
||||
capabilities.push('tools');
|
||||
}
|
||||
|
||||
let inputModalities = info.modalities?.input.filter((m: string) => (SupportedModalities as Readonly<string[]>).includes(m));
|
||||
if (inputModalities === undefined || inputModalities.length === 0) {
|
||||
inputModalities = ['text'];
|
||||
}
|
||||
|
||||
let outputModalities = info.modalities?.output.filter((m: string) => (SupportedModalities as Readonly<string[]>).includes(m));
|
||||
if (outputModalities === undefined || outputModalities.length === 0) {
|
||||
outputModalities = ['text'];
|
||||
}
|
||||
|
||||
// merge pModel.attributes and info.modalities, with a preference for pModel.attributes
|
||||
const attributes = {
|
||||
inputModalities: new Set(info.modalities?.input.filter(m => ['text', 'image'].includes(m)) || ['text']),
|
||||
outputModalities: new Set(info.modalities?.output.filter(m => ['text', 'image'].includes(m)) || ['text']),
|
||||
inputModalities: new Set(inputModalities),
|
||||
outputModalities: new Set(outputModalities),
|
||||
capabilities,
|
||||
contextWindow: pModel.context_length || info.limit?.context || null,
|
||||
supported_parameters: new Set(pModel.supported_parameters || ["temperature", "max_tokens"]),
|
||||
...(pModel.attributes || {}),
|
||||
};
|
||||
|
||||
let cost;
|
||||
if (pModel.pricing === undefined) {
|
||||
cost = {}
|
||||
} else {
|
||||
cost = {
|
||||
prompt: pModel.pricing.prompt || null,
|
||||
completion: pModel.pricing.completion || null,
|
||||
request: pModel.pricing.request || null,
|
||||
image: pModel.pricing.image || null,
|
||||
imageTokens: pModel.pricing.image_tokens || null,
|
||||
imageOutput: pModel.pricing.image_output || null,
|
||||
audio: pModel.pricing.audio || null,
|
||||
audioOutput: pModel.pricing.audio_output || null,
|
||||
inputAudioCache: pModel.pricing.input_audio_cache || null,
|
||||
webSearch: pModel.pricing.web_search || null,
|
||||
internalReasoning: pModel.pricing.internal_reasoning || null,
|
||||
inputCacheRead: pModel.pricing.input_cache_read || null,
|
||||
inputCacheWrite: pModel.pricing.input_cache_write || null,
|
||||
discount: pModel.pricing.discount || null,
|
||||
}
|
||||
}
|
||||
|
||||
const existing = existingModelsMap.get(pModel.id);
|
||||
|
||||
if (existing) {
|
||||
@@ -143,10 +199,10 @@ const fetchModels = async () => {
|
||||
id: existing.id,
|
||||
data: {
|
||||
...existingWithoutId,
|
||||
name: existing.name || info.name || pModel.name || pModel.id,
|
||||
attributes: attributes, // Update tech specs
|
||||
releasedAt: new Date(pModel.created * 1000),
|
||||
updatedAt: new Date()
|
||||
name: existing.name || pModel.name || info.name || pModel.id,
|
||||
cost,
|
||||
attributes, // Update tech specs
|
||||
releasedAt: pModel.created ? new Date(pModel.created * 1000) : null,
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@@ -155,22 +211,29 @@ const fetchModels = async () => {
|
||||
userId: user.value?.id,
|
||||
providerId: provider.value!.id,
|
||||
externalId: pModel.id,
|
||||
name: info.name || pModel.name || pModel.id,
|
||||
name: pModel.name || info.name || pModel.id,
|
||||
isCustom: false,
|
||||
enabled: false,
|
||||
attributes: attributes,
|
||||
releasedAt: new Date(pModel.created * 1000),
|
||||
cost,
|
||||
attributes,
|
||||
releasedAt: pModel.created ? new Date(pModel.created * 1000) : null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// delete models that are not in the API response and are not custom models
|
||||
const apiModelIds = new Set(providerResponse.models.map((p: any) => p.id));
|
||||
const toDelete = (provider.value?.models || []).filter((m: any) =>
|
||||
!m.isCustom && !apiModelIds.has(m.externalId)
|
||||
);
|
||||
|
||||
console.log({ toUpdate, toInsert, toDelete });
|
||||
|
||||
await Promise.all([
|
||||
...toInsert.map(item => triplit.insert('models', item)),
|
||||
...toUpdate.map(item => triplit.update('models', item.id, (m) => {
|
||||
Object.assign(m, item.data);
|
||||
}))
|
||||
...toUpdate.map(item => triplit.update('models', item.id, item.data)),
|
||||
...toDelete.map(item => triplit.delete('models', item.id))
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch models:', error);
|
||||
@@ -179,14 +242,34 @@ const fetchModels = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const deleteModels = async () => {
|
||||
if (!provider.value) return;
|
||||
|
||||
await Promise.all(provider.value.models.map(m => triplit.delete('models', m.id)));
|
||||
};
|
||||
|
||||
const enabledModels = computed(() =>
|
||||
filterModels(provider.value?.models.filter(m => m.enabled === true) as Model[] || [], modelSearch.value)
|
||||
.sort((a, b) => a.releasedAt && b.releasedAt ? b.releasedAt.getTime() - a.releasedAt.getTime() : 0)
|
||||
)
|
||||
|
||||
const disabledModels = computed(() =>
|
||||
filterModels(provider.value?.models.filter(m => m.enabled === false) as Model[] || [], modelSearch.value)
|
||||
.sort((a, b) => a.releasedAt && b.releasedAt ? b.releasedAt.getTime() - a.releasedAt.getTime() : 0)
|
||||
)
|
||||
|
||||
onUnmounted(() => {
|
||||
unsubscribeModels?.();
|
||||
});
|
||||
|
||||
defineEmits(['navigate']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-4 mt-4">
|
||||
<div class="flex flex-col gap-4 mt-4" v-if="provider">
|
||||
<div class="flex flex-row justify-between gap-16">
|
||||
<label class="whitespace-nowrap" for="provider-api-key">Enabled</label>
|
||||
<Slider :checked="provider!.enabled" @click.stop="toggleProvider()" />
|
||||
<Slider :checked="provider.enabled" @click.stop="toggleProvider()" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row justify-between gap-16">
|
||||
@@ -194,11 +277,11 @@ defineEmits(['navigate']);
|
||||
<div
|
||||
class="text-sm font-mono flex flex-row rounded-md bg-[var(--color-highlight)] items-center gap-1 w-7/10">
|
||||
<input class="w-full p-0 pl-2 py-1 bg-transparent" :type="apiKeyVisible ? 'text' : 'password'"
|
||||
id="provider-api-key" :value="apiKey"
|
||||
id="provider-api-key" :value="apiKey" autocomplete="false" spellcheck="false"
|
||||
@input="updateApiKey(($event.target! as HTMLInputElement).value)" />
|
||||
<button @click="apiKeyVisible = !apiKeyVisible"
|
||||
class="text-sm p-2 text-[var(--color-muted)] hover:text-[var(--color-text)]">
|
||||
<Icon :name="apiKeyVisible ? 'mynaui:eye' : 'mynaui:eye-slash'" class="text-4" />
|
||||
<Icon :name="apiKeyVisible ? 'mynaui:eye' : 'mynaui:eye-slash'" class="text-4 min-h-4 min-w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -207,8 +290,8 @@ defineEmits(['navigate']);
|
||||
<label class="whitespace-nowrap" for="provider-api-key">API Proxy URL</label>
|
||||
<div
|
||||
class="text-sm font-mono flex flex-row rounded-md bg-[var(--color-highlight)] items-center gap-1 w-7/10">
|
||||
<input :placeholder="providerBaseUrls[provider!.type]" class="w-full px-2 py-1 bg-transparent"
|
||||
:type="apiKeyVisible ? 'text' : 'password'" id="provider-api-key" :value="apiProxyUrl"
|
||||
<input :placeholder="providerBaseUrls[provider!.type] ?? ''" class="w-full px-2 py-1 bg-transparent"
|
||||
type="text" id="provider-proxy-url" :value="apiProxyUrl"
|
||||
@input="updateProxyUrl(($event.target! as HTMLInputElement).value)" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -221,17 +304,26 @@ defineEmits(['navigate']);
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col">
|
||||
<div class="pt-5 justify-between w-full flex">
|
||||
<h4 class="whitespace-nowrap m-0">
|
||||
<div class="pt-5 justify-between w-full flex flex-wrap gap-y-1 items-center">
|
||||
<h4 class="whitespace-nowrap m-0 flex gap-x-2 items-start">
|
||||
Model List
|
||||
<span class="text-sm text-[var(--color-muted)] font-normal text-xs">
|
||||
{{ provider?.models.length }} models available
|
||||
<span class="text-sm text-[var(--color-muted)] font-normal text-xs flex items-center gap-1">
|
||||
{{ provider?.models.length }} models available <button @click="deleteModels">
|
||||
<Icon name="mynaui:x-solid" />
|
||||
</button>
|
||||
</span>
|
||||
</h4>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<input v-model="modelSearch" type="text" class="px-2 py-1 bg-[var(--color-highlight)] text-xs"
|
||||
placeholder="Search models..." />
|
||||
<div
|
||||
class="flex items-center justify-center px-2 py-1 bg-[var(--color-highlight)] text-xs rounded-md">
|
||||
<input class="p-0 bg-transparent" v-model="modelSearch" type="text"
|
||||
placeholder="Search models..." />
|
||||
<button :class="modelSearch.length > 0 ? 'visible' : 'invisible'" @click="modelSearch = ''"
|
||||
class="right-1 hover:bg-[var(--color-highlight)] rounded p-0.5">
|
||||
<Icon name="mynaui:x" class="text-3.5 block text-[var(--color-subtle)]" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button @click="fetchModels"
|
||||
class="whitespace-nowrap flex bg-[var(--color-highlight)] hover:bg-[var(--color-highlight-high)] text-sm rounded-md items-center px-2 py-0.5 gap-2 transition-colors duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
|
||||
@@ -248,70 +340,46 @@ defineEmits(['navigate']);
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-else class="flex flex-col gap-1 mt-2">
|
||||
<span class="text-sm text-[var(--color-muted)]">
|
||||
Enabled
|
||||
</span>
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="p-3 flex items-center justify-between"
|
||||
v-for="model in provider?.models.filter(m => m.enabled === true).filter(m => !modelSearch || m.name.toLowerCase().includes(modelSearch.toLowerCase()))"
|
||||
:key="model.id">
|
||||
<div class="flex flex-row items-center">
|
||||
<div class="flex items-center">
|
||||
<ModelIcon :avatar="true" variant="color" :model-id="model.externalId" size="32" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-1 ml-2">
|
||||
<div
|
||||
class="text-sm font-medium text-ellipsis overflow-hidden text-[var(--color-text)] flex items-center gap-1">
|
||||
{{ model.name }}
|
||||
<span
|
||||
class="text-xs text-[var(--color-muted)] px-1 py-0.5 rounded bg-[var(--color-highlight)]">
|
||||
{{ model.externalId }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-xs text-[var(--color-muted)]">
|
||||
Released on {{
|
||||
model.releasedAt?.toISOString().split('T')[0] }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Slider :checked="model.enabled" @click="toggleModel(model.id)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="text-sm text-[var(--color-muted)]">
|
||||
Disabled
|
||||
<ClientOnly v-else>
|
||||
<div class="flex flex-col gap-1 mt-2">
|
||||
<span v-if="enabledModels.length > 0" class="text-sm text-[var(--color-muted)]">
|
||||
Enabled
|
||||
</span>
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="p-3 flex items-center justify-between"
|
||||
v-for="model in provider?.models.filter(m => m.enabled === false).filter(m => !modelSearch || m.name.toLowerCase().includes(modelSearch.toLowerCase()))"
|
||||
:key="model.id">
|
||||
<div class="flex flex-row items-center">
|
||||
<div class="flex items-center">
|
||||
<ModelIcon :avatar="true" variant="color" :model-id="model.externalId" size="32" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-1 ml-2">
|
||||
<div
|
||||
class="text-sm font-medium text-ellipsis overflow-hidden text-[var(--color-text)] flex items-center gap-1">
|
||||
{{ model.name }}
|
||||
<span
|
||||
class="text-xs text-[var(--color-muted)] px-1 py-0.5 rounded bg-[var(--color-highlight)]">
|
||||
{{ model.externalId }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-xs text-[var(--color-muted)]">
|
||||
Released on {{ model.releasedAt?.toISOString().split('T')[0] }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Slider :checked="model.enabled" @click="toggleModel(model.id)" />
|
||||
</div>
|
||||
<DynamicScroller class="scroller" page-mode :min-item-size="64" :buffer="640"
|
||||
:items="enabledModels" key-field="id">
|
||||
<template v-slot="{ item: model, index, active }">
|
||||
<DynamicScrollerItem :item="model" :active="active" :size-dependencies="[
|
||||
model.name,
|
||||
model.externalId,
|
||||
modelSearch
|
||||
]" :data-index="index">
|
||||
<ModelItem :model="model" />
|
||||
</DynamicScrollerItem>
|
||||
</template>
|
||||
</DynamicScroller>
|
||||
|
||||
<span v-if="disabledModels.length > 0" class="text-sm text-[var(--color-muted)]">
|
||||
Disabled
|
||||
</span>
|
||||
<div class="flex flex-col gap-1">
|
||||
<DynamicScroller class="scroller" page-mode :min-item-size="64" :buffer="640"
|
||||
:items="disabledModels" key-field="id">
|
||||
<template v-slot="{ item: model, index, active }">
|
||||
<DynamicScrollerItem :item="model" :active="active" :size-dependencies="[
|
||||
model.name,
|
||||
model.externalId,
|
||||
model.cost,
|
||||
modelSearch
|
||||
]" :data-index="index">
|
||||
<ModelItem :model="model" />
|
||||
</DynamicScrollerItem>
|
||||
</template>
|
||||
</DynamicScroller>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ClientOnly>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
|
||||
</template>
|
||||
@@ -2,13 +2,11 @@
|
||||
import GeneralSettings from './GeneralSettings.vue';
|
||||
import ProviderSettings from './ProviderSettings.vue';
|
||||
import ProviderSidebar from './ProviderSidebar.vue';
|
||||
import SystemAssistants from './SystemAssistants.vue';
|
||||
import AppearanceSettings from './AppearanceSettings.vue';
|
||||
import AIServiceProvider from './AIServiceProvider.vue';
|
||||
|
||||
const { providers } = await useModels();
|
||||
|
||||
const { currentPage, pageParams, open, setPage, close } = useSettings();
|
||||
|
||||
console.log(providers.value);
|
||||
const { providers, unsubscribe: unsubscribeModels } = await useModels();
|
||||
|
||||
const PAGES_CONFIG = {
|
||||
general: {
|
||||
@@ -16,14 +14,26 @@ const PAGES_CONFIG = {
|
||||
icon: 'mynaui:cog-four',
|
||||
component: GeneralSettings
|
||||
},
|
||||
appearance: {
|
||||
label: 'Appearance',
|
||||
icon: 'tabler:palette',
|
||||
component: AppearanceSettings
|
||||
},
|
||||
providers: {
|
||||
label: 'AI Providers',
|
||||
icon: 'mynaui:api',
|
||||
component: ProviderSettings,
|
||||
sidebar: ProviderSidebar
|
||||
},
|
||||
systemAssistants: {
|
||||
label: 'System Assistants',
|
||||
icon: 'mynaui:sparkles',
|
||||
component: SystemAssistants
|
||||
},
|
||||
} as const;
|
||||
|
||||
const { currentPage, pageParams, open, setPage, close } = useSettings();
|
||||
|
||||
const runtimePage = computed(() => {
|
||||
// 1. Get the base config (e.g., 'providers' or 'general')
|
||||
const config = PAGES_CONFIG[currentPage.value as keyof typeof PAGES_CONFIG] || PAGES_CONFIG.general;
|
||||
@@ -35,7 +45,6 @@ const runtimePage = computed(() => {
|
||||
if (currentPage.value === 'providers' && pageParams.value.length > 0) {
|
||||
component = AIServiceProvider;
|
||||
const providerId = pageParams.value[0];
|
||||
console.log("PROVIDERS", providers.value);
|
||||
const provider = providers.value!.find(p => p.id === providerId);
|
||||
label = provider ? provider.name : 'Unknown Provider';
|
||||
}
|
||||
@@ -72,6 +81,7 @@ onUnmounted(() => {
|
||||
if (open.value) {
|
||||
document.body.removeEventListener('keydown', handleKeyDown);
|
||||
}
|
||||
unsubscribeModels?.();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -88,7 +98,7 @@ onUnmounted(() => {
|
||||
<div v-if="open" class="z-50 fixed top-1/2 left-1/2 -translate-x-1/2 flex items-center justify-center">
|
||||
<div class="absolute w-[85vw] max-w-6xl h-[70vh] bg-[var(--color-base)] rounded-xl shadow-2xl border border-[var(--color-highlight)]
|
||||
overflow-hidden flex max-h-[90vh] p-2">
|
||||
<nav class="w-64 flex flex-col gap-1 mr-2">
|
||||
<nav class="w-64 flex flex-col gap-1 mr-2 overflow-y-auto">
|
||||
<!-- If the page has a custom sidebar (for nested lists), show it; otherwise show default nav -->
|
||||
<component v-if="runtimePage?.sidebar" :is="runtimePage.sidebar" @navigate="setPage" />
|
||||
|
||||
@@ -104,7 +114,7 @@ onUnmounted(() => {
|
||||
<!-- DYNAMIC CONTENT -->
|
||||
<main class="flex-1 flex flex-col overflow-hidden">
|
||||
<div
|
||||
class="flex-1 p-3 bg-[var(--color-neutral)] overflow-y-auto border rounded-lg border-[var(--color-highlight)]">
|
||||
class="flex flex-col flex-1 p-3 bg-[var(--color-neutral)] overflow-y-auto border rounded-lg border-[var(--color-highlight)]">
|
||||
<header class="flex items-center justify-between pl-2 pb-2 ">
|
||||
<h2 class="text-lg font-semibold m-0">{{ runtimePage.label }}</h2>
|
||||
<button
|
||||
@@ -113,10 +123,7 @@ onUnmounted(() => {
|
||||
<Icon name="mynaui:x-solid" />
|
||||
</button>
|
||||
</header>
|
||||
<!-- KeepAlive preserves state if the user clicks back/forth between tabs -->
|
||||
<KeepAlive>
|
||||
<component @navigate="setPage" :is="runtimePage.component" />
|
||||
</KeepAlive>
|
||||
<component @navigate="setPage" :is="runtimePage.component" />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<script lang="ts" setup>
|
||||
const triplit = useTriplitClient();
|
||||
|
||||
const props = defineProps<{
|
||||
model: ModelWithProvider;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="p-3 text-white flex max-w-full items-center justify-between gap-2 group hover:bg-[var(--color-highlight-low)] transition duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
|
||||
<ModelInfo :model="model" :show-edit="true" :show-cost="true" :show-external-id="true"
|
||||
:show-release-date="true" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,6 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import { Providers } from '~/types/model';
|
||||
|
||||
const triplit = useTriplitClient();
|
||||
const { providers } = await useModels();
|
||||
const { providers, unsubscribe: unsubscribeModels } = await useModels();
|
||||
|
||||
if (providers.value === undefined) throw new Error('Providers not loaded');
|
||||
|
||||
const { user } = useAuth();
|
||||
for (const provider of Providers) {
|
||||
if (!providers.value?.find(p => p.type === provider)) {
|
||||
// create a new provider
|
||||
await triplit.insert('providers', {
|
||||
name: provider,
|
||||
userId: user.value!.id,
|
||||
type: provider,
|
||||
enabled: false,
|
||||
config: {},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const toggleProvider = async (id: string) => {
|
||||
const provider = providers.value!.find(p => p.id === id);
|
||||
@@ -11,6 +29,10 @@ const toggleProvider = async (id: string) => {
|
||||
});
|
||||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
unsubscribeModels?.();
|
||||
});
|
||||
|
||||
defineEmits(['navigate']);
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
const { pageParams } = useSettings();
|
||||
const { providers } = await useModels();
|
||||
const { providers, unsubscribe: unsubscribeModels } = await useModels();
|
||||
|
||||
console.log("PROVIDERS", providers.value);
|
||||
onUnmounted(() => {
|
||||
unsubscribeModels?.();
|
||||
});
|
||||
|
||||
defineEmits(['navigate']);
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<script setup lang="ts">
|
||||
const triplit = useTriplitClient();
|
||||
const { providers, unsubscribe: unsubscribeModels, allModels } = await useModels();
|
||||
const { settings, unsubscribe: unsubscribeSettings } = await useUserSettings();
|
||||
|
||||
const toggle = async (key: string) => {
|
||||
console.log(key);
|
||||
|
||||
await triplit.update('settings', settings.value.id, {
|
||||
systemAssistants: {
|
||||
...settings.value.systemAssistants,
|
||||
[key]: {
|
||||
// @ts-ignore
|
||||
...settings.value.systemAssistants[key],
|
||||
// @ts-ignore
|
||||
enabled: !settings.value.systemAssistants[key].enabled
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const updateModel = async (key: string, model: ModelWithProvider | undefined | null) => {
|
||||
await triplit.update('settings', settings.value.id, {
|
||||
systemAssistants: {
|
||||
...settings.value.systemAssistants,
|
||||
[key]: {
|
||||
// @ts-ignore
|
||||
...settings.value.systemAssistants[key],
|
||||
modelId: model?.id ?? null
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const getModel = (id: string | null | undefined) => {
|
||||
if (!id) return null;
|
||||
|
||||
return allModels.value.find(m => m.id === id);
|
||||
}
|
||||
|
||||
defineEmits(['navigate']);
|
||||
|
||||
onUnmounted(() => {
|
||||
unsubscribeModels?.();
|
||||
unsubscribeSettings?.();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-4 flex-grow">
|
||||
<div class="flex flex-col" v-for="(systemAssistant, key) in settings.systemAssistants">
|
||||
<label :for="`slider-${key}`" class="flex justify-between gap-4 items-center">
|
||||
<h4 class="capitalize">{{ key }}</h4>
|
||||
<Slider :id="`slider-${key}`" :checked="systemAssistant.enabled" @click="toggle(key)" />
|
||||
</label>
|
||||
<div class="flex-1 justify-between gap-4 items-center">
|
||||
<ModelSelector :providers="providers" :model-value="getModel(systemAssistant.modelId)"
|
||||
@update:model-value="(model) => updateModel(key, model)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,23 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownItem } from '~/types/dropdown';
|
||||
import { authClient } from '~~/lib/auth-client';
|
||||
import { assert } from '~~/utils/assert';
|
||||
|
||||
const triplit = useTriplitClient();
|
||||
const { user } = await useAuth();
|
||||
const { user, signOut } = useAuth();
|
||||
|
||||
// to prevent the user details from going blank for a
|
||||
// single frame when the user logs out (yes I am that
|
||||
// particular)
|
||||
const cachedUser = ref(user.value);
|
||||
|
||||
watch(user, () => {
|
||||
if (user.value === null) return;
|
||||
|
||||
cachedUser.value = user.value;
|
||||
})
|
||||
|
||||
const { toggle: toggleSettings } = useSettings();
|
||||
|
||||
const hovering = defineModel<boolean>({ required: true });
|
||||
const { isHovered } = useSidenavContext();
|
||||
|
||||
const profileOpen = ref(false);
|
||||
|
||||
const handleLogout = async () => {
|
||||
await authClient.signOut();
|
||||
if ('endSession' in triplit) {
|
||||
await triplit.endSession();
|
||||
}
|
||||
|
||||
clearNuxtData();
|
||||
await signOut();
|
||||
assert('disconnect' in triplit);
|
||||
triplit.disconnect();
|
||||
|
||||
await navigateTo('/auth/login');
|
||||
};
|
||||
@@ -43,15 +51,15 @@ const profileItems: DropdownItem[] = [
|
||||
@click="toggle">
|
||||
<div
|
||||
:class="['w-[28px] h-[28px] flex-shrink-0 rounded-lg overflow-hidden bg-[var(--color-neutral)] flex items-center justify-center', user?.image ? '' : 'border border-[var(--color-highlight-high)]']">
|
||||
<img v-if="user?.image" :src="user.image" class="w-full h-full object-cover" />
|
||||
<img v-if="cachedUser?.image" :src="cachedUser.image" class="w-full h-full object-cover" />
|
||||
<Icon v-else name="mynaui:user" class="w-4 h-4 text-[var(--color-muted)]" />
|
||||
</div>
|
||||
<span
|
||||
class="text-sm font-medium text-ellipsis overflow-hidden text-[var(--color-text)] whitespace-nowrap">{{
|
||||
user!.name
|
||||
cachedUser?.name
|
||||
}}</span>
|
||||
<div :class="['flex-shrink-0 w-4 h-4 text-[var(--color-muted)] transition-all duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)] overflow-hidden transform-origin-center-left',
|
||||
hovering ? 'opacity-100 scale-100' : 'opacity-0 scale-x-0 scale-y-90'
|
||||
<div :class="['transform-origin-left-center flex-shrink-0 w-4 h-4 text-[var(--color-muted)] transition-all duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)] overflow-hidden transform-origin-center-left',
|
||||
isHovered ? 'opacity-100 scale-100' : 'opacity-0 scale-40'
|
||||
]">
|
||||
<Icon class="text-4" name="mynaui:chevron-down" />
|
||||
</div>
|
||||
|
||||
@@ -2,47 +2,14 @@
|
||||
import type { DropdownItem } from '~/types/dropdown';
|
||||
|
||||
const route = useRoute();
|
||||
const { agents, getAgent } = await useAgents();
|
||||
const homeButtonRef = ref<HTMLElement | null>(null);
|
||||
const { agents, getAgent, unsubscribe: unsubscribeAgents } = await useAgents();
|
||||
|
||||
const activeAgent = computed(() => getAgent(route.params.id as string));
|
||||
|
||||
const hovering = defineModel<boolean>({ required: true });
|
||||
const initialized = ref(false);
|
||||
|
||||
let lastHovering: boolean | null = null;
|
||||
onMounted(() => {
|
||||
console.log(hovering.value);
|
||||
|
||||
if (hovering.value && homeButtonRef.value) {
|
||||
const width = homeButtonRef.value.scrollWidth;
|
||||
homeButtonRef.value.style.width = `calc(${width}px + 0.5rem)`;
|
||||
}
|
||||
|
||||
watch(hovering, (value) => {
|
||||
if (lastHovering === value) {
|
||||
console.warn('Hovering value did not change, but watcher was triggered');
|
||||
}
|
||||
console.log(value, lastHovering);
|
||||
lastHovering = value;
|
||||
|
||||
if (!initialized.value) {
|
||||
initialized.value = true;
|
||||
}
|
||||
|
||||
if (!homeButtonRef.value) return;
|
||||
|
||||
if (value) {
|
||||
const width = homeButtonRef.value.scrollWidth;
|
||||
homeButtonRef.value.style.width = `calc(${width}px + 0.5rem)`;
|
||||
} else {
|
||||
homeButtonRef.value.style.width = '0';
|
||||
}
|
||||
});
|
||||
});
|
||||
const { isHovered } = useSidenavContext();
|
||||
|
||||
onUnmounted(() => {
|
||||
console.log('unmounted');
|
||||
unsubscribeAgents?.();
|
||||
});
|
||||
|
||||
const agentDropdownOpen = ref(false);
|
||||
@@ -52,8 +19,8 @@ const agentItems: DropdownItem[] = [];
|
||||
|
||||
<template>
|
||||
<header class="flex items-center rounded-lg overflow-hidden">
|
||||
<div ref="homeButtonRef" style="width: 0;"
|
||||
:class="['flex flex-shrink-0 items-center overflow-hidden', initialized ? 'transition-all duration-150 ease-[cubic-bezier(0.5,_1,_0.89,_1)]' : '', hovering ? 'opacity-100 scale-100' : 'opacity-0 scale-95']">
|
||||
<div :style="isHovered ? 'width: 32px;' : 'width: 0px;'"
|
||||
:class="['flex flex-shrink-0 transform-origin-left-center items-center overflow-hidden transition-all duration-150 ease-[cubic-bezier(0.5,_1,_0.89,_1)]', isHovered ? 'opacity-100 scale-100' : 'opacity-0 scale-95']">
|
||||
<NuxtLink to="/"
|
||||
class="flex hover:bg-[var(--color-highlight)] focus-visible:bg-[var(--color-highlight)] rounded-lg decoration-none transition-inherit text-[var(--color-muted)] p-1.5">
|
||||
<Icon name="mynaui:chevron-left" class="w-4.5 h-4.5" />
|
||||
|
||||
@@ -4,7 +4,7 @@ const topicsListRef = ref<HTMLElement | null>(null);
|
||||
const topicsListHeight = ref('auto');
|
||||
const topicsListOpacity = ref(1);
|
||||
const topicsListScale = ref(1);
|
||||
const { getAgent } = await useAgents();
|
||||
const { getAgent, unsubscribe: unsubscribeAgents } = await useAgents();
|
||||
|
||||
const triplit = useTriplitClient();
|
||||
|
||||
@@ -88,6 +88,19 @@ const toggleAgentsList = () => {
|
||||
requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
const autoRenameTopic = async (topicId: string) => {
|
||||
const { setPage } = useSettings();
|
||||
const { autoRename } = useChat(route.params.id as string);
|
||||
|
||||
const firstMessage = await triplit.fetchOne(triplit.query('messages').Where('topicId', '=', topicId).Order('createdAt', 'ASC').Limit(1));
|
||||
if (!firstMessage) return;
|
||||
|
||||
const success = await autoRename(topicId, firstMessage.content);
|
||||
if (!success) {
|
||||
setPage('systemAssistants');
|
||||
}
|
||||
}
|
||||
|
||||
const renameTopic = (topicId: string) => {
|
||||
console.log('renameTopic', topicId);
|
||||
};
|
||||
@@ -101,7 +114,13 @@ const deleteTopic = async (topicId: string) => {
|
||||
}
|
||||
}
|
||||
await triplit.delete('topics', topicId);
|
||||
|
||||
// TODO: deeply delete all messages, generations, and message_parts in the topic
|
||||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
unsubscribeAgents?.();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -119,38 +138,58 @@ const deleteTopic = async (topicId: string) => {
|
||||
<Icon name="mynaui:chevron-down" :class="['w-4 h-4', topicsOpen ? '' : '-rotate-90']" />
|
||||
</button>
|
||||
|
||||
<div ref="topicsListRef" :inert="!topicsOpen"
|
||||
<div ref="topicsListRef" :inert="!topicsOpen" :class="{ 'overflow-y-hidden': !topicsOpen }"
|
||||
:style="{ height: topicsListHeight, opacity: topicsListOpacity, transform: `scale(${topicsListScale})` }"
|
||||
class="mt-1 gap-1 flex flex-col transform-origin-center-top overflow-y-hidden">
|
||||
<SidenavItem draggable="false" class="[&>div>div>div>[dots]]:hover:opacity-100 relative"
|
||||
v-if="activeAgent?.topics !== undefined" v-for="topic in topics"
|
||||
:to="`/agent/${activeAgent.id}/topic/${topic.id}`" :active="topic.id === route.params.topicId"
|
||||
:name="topic.name" :key="topic.id">
|
||||
<Dropdown class="shrink-0" verticality="descending" placement="right">
|
||||
<template #trigger="{ toggle, isOpen }">
|
||||
<div dots @click.prevent.stop="toggle"
|
||||
class="opacity-0 p-1 flex items-center justify-center rounded-md hover:bg-[var(--color-highlight)] focus-visible:bg-[var(--color-highlight)] transition-all duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18"
|
||||
viewBox="0 0 24 24"><!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ -->
|
||||
<path fill="currentColor"
|
||||
d="M7 12a2 2 0 1 1-4 0a2 2 0 0 1 4 0m7 0a2 2 0 1 1-4 0a2 2 0 0 1 4 0m7 0a2 2 0 1 1-4 0a2 2 0 0 1 4 0" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="shadow-lg rounded p-1 flex flex-col min-w-[120px] gap-1">
|
||||
<button @click.prevent="renameTopic(topic.id)"
|
||||
class="text-left px-3 py-1.5 text-sm hover:bg-[var(--color-highlight)] rounded-lg transition-colors duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
|
||||
Rename
|
||||
</button>
|
||||
<button @click.prevent="deleteTopic(topic.id)"
|
||||
class="text-left px-3 py-1.5 text-sm text-red-600 hover:bg-red-600/20 rounded-lg transition-colors duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</SidenavItem>
|
||||
class="mt-1 gap-1 flex flex-col transform-origin-center-top">
|
||||
<NuxtLink v-if="activeAgent?.topics !== undefined" v-for="topic in topics"
|
||||
:to="`/agent/${activeAgent.id}/topic/${topic.id}`" :aria-label="topic.name" :class="[
|
||||
'group relative decoration-none text-[var(--color-muted)] flex justify-between items-center shrink-0 rounded-lg transition-colors cursor-pointer h-9',
|
||||
'px-2',
|
||||
topic.id === route.params.topicId
|
||||
? 'text-[var(--color-text)] bg-[var(--color-highlight)] hover:bg-[var(--color-highlight-high)] focus-visible:bg-[var(--color-highlight-high)]'
|
||||
: 'hover:bg-[var(--color-highlight)] focus-visible:bg-[var(--color-highlight)]'
|
||||
]">
|
||||
<div class="flex items-center gap-2 max-w-full flex-1">
|
||||
<div v-if="!topic.renaming" class="flex justify-between items-center w-full">
|
||||
<span class="text-sm font-medium overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{{ topic.name }}
|
||||
</span>
|
||||
|
||||
<Dropdown class="shrink-0 text-[var(--color-text)]" verticality="descending"
|
||||
placement="right">
|
||||
<template #trigger="{ toggle }">
|
||||
<div dots @click.prevent="toggle"
|
||||
class="opacity-0 group-hover:opacity-100 p-1 flex items-center justify-center rounded-md hover:bg-[var(--color-highlight)] focus-visible:bg-[var(--color-highlight)] transition-all duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18"
|
||||
viewBox="0 0 24 24"><!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ -->
|
||||
<path fill="currentColor"
|
||||
d="M7 12a2 2 0 1 1-4 0a2 2 0 0 1 4 0m7 0a2 2 0 1 1-4 0a2 2 0 0 1 4 0m7 0a2 2 0 1 1-4 0a2 2 0 0 1 4 0" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
<template #content="{ toggle }">
|
||||
<div class="shadow-lg rounded p-1 flex flex-col min-w-[120px] gap-1">
|
||||
<button @click.prevent="autoRenameTopic(topic.id); toggle()"
|
||||
class="text-left px-3 py-1.5 text-sm hover:bg-[var(--color-highlight)] rounded-lg transition-colors duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
|
||||
Auto Rename
|
||||
</button>
|
||||
<button @click.prevent="renameTopic(topic.id); toggle()"
|
||||
class="text-left px-3 py-1.5 text-sm hover:bg-[var(--color-highlight)] rounded-lg transition-colors duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
|
||||
Rename
|
||||
</button>
|
||||
<button @click.prevent="deleteTopic(topic.id); toggle()"
|
||||
class="text-left px-3 py-1.5 text-sm text-red-600 hover:bg-red-600/20 rounded-lg transition-colors duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<div v-else class="flex w-full">
|
||||
<Icon name="svg-spinners:3-dots-fade" class="text-6" />
|
||||
</div>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
const { agents } = await useAgents();
|
||||
const { agents, unsubscribe: unsubscribeAgents, createAgent } = await useAgents();
|
||||
const agentsListRef = ref<HTMLElement | null>(null);
|
||||
const agentsOpen = ref(true);
|
||||
const agentsListHeight = ref('auto');
|
||||
@@ -62,30 +62,15 @@ const newAgent = async () => {
|
||||
|
||||
if (!user.value) throw new Error('User not logged in');
|
||||
|
||||
const agent = await triplit.insert('agents', {
|
||||
name: 'New Agent',
|
||||
userId: user.value.id,
|
||||
systemPrompt: 'You are a helpful assistant.',
|
||||
imageUrl: null,
|
||||
createdAt: new Date(),
|
||||
});
|
||||
|
||||
console.log(agents.value, agent);
|
||||
const agent = await createAgent();
|
||||
if (!agent) throw new Error('Failed to create agent');
|
||||
|
||||
let agentExists: () => void;
|
||||
const agentExistsPromise = new Promise<void>((resolve) => {
|
||||
agentExists = resolve;
|
||||
});
|
||||
|
||||
watch(agents, () => {
|
||||
agentExists();
|
||||
});
|
||||
|
||||
await agentExistsPromise;
|
||||
|
||||
navigateTo(`/agent/${agent.id}`);
|
||||
return navigateTo(`/agent/${agent.id}`);
|
||||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
unsubscribeAgents?.();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -6,8 +6,34 @@ const isResizing = ref(false);
|
||||
const startX = ref(0);
|
||||
const initialWidth = ref(0);
|
||||
|
||||
const isMouseOver = ref(false);
|
||||
const isFocused = ref(false);
|
||||
const lastInteraction = ref<'mouse' | 'keyboard' | null>(null);
|
||||
|
||||
const isHovering = computed(() =>
|
||||
isMouseOver.value || (isFocused.value && lastInteraction.value === 'keyboard')
|
||||
);
|
||||
const sidenavRef = ref<HTMLElement | null>(null);
|
||||
const closeSidenavRef = ref<HTMLElement | null>(null);
|
||||
|
||||
provideSidenavContext({
|
||||
isHovered: isHovering,
|
||||
sidebarWidth: sidebarWidth,
|
||||
isOpen: open,
|
||||
close: () => {
|
||||
closeSidebar();
|
||||
},
|
||||
});
|
||||
|
||||
const trackInteraction = (interaction: 'mouse' | 'keyboard') => {
|
||||
lastInteraction.value = interaction;
|
||||
};
|
||||
|
||||
const onFocusOut = (e: FocusEvent) => {
|
||||
const isMovingOutside = sidenavRef.value && !sidenavRef.value.contains(e.relatedTarget as Node);
|
||||
if (isMovingOutside) {
|
||||
isFocused.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const { toggle: toggleSettings } = useSettings();
|
||||
|
||||
@@ -46,63 +72,55 @@ onMounted(() => {
|
||||
document.addEventListener('mousemove', onResizeMove);
|
||||
document.addEventListener('mouseup', onResizeEnd);
|
||||
|
||||
watch(hovering, (value) => {
|
||||
if (!closeSidenavRef.value) return;
|
||||
|
||||
if (value) {
|
||||
const width = closeSidenavRef.value.scrollWidth;
|
||||
closeSidenavRef.value.style.width = `${width}px`;
|
||||
} else {
|
||||
closeSidenavRef.value.style.width = '0';
|
||||
}
|
||||
// NEW: Track global interactions
|
||||
document.addEventListener('mousedown', () => trackInteraction('mouse'));
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Tab') trackInteraction('keyboard');
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('mousemove', onResizeMove);
|
||||
document.removeEventListener('mouseup', onResizeEnd);
|
||||
});
|
||||
|
||||
const hovering = ref(false);
|
||||
// NEW: Cleanup listeners
|
||||
document.removeEventListener('mousedown', () => trackInteraction('mouse'));
|
||||
document.removeEventListener('keydown', (e) => {
|
||||
if (e.key === 'Tab') trackInteraction('keyboard');
|
||||
});
|
||||
});
|
||||
|
||||
const navKind = computed(() => {
|
||||
if (route.path === '/') return 'home';
|
||||
if (route.path.startsWith('/agent/')) return 'agent';
|
||||
return null;
|
||||
});
|
||||
|
||||
const onFocusOut = (e: FocusEvent) => {
|
||||
const isMovingOutside = sidenavRef.value && !sidenavRef.value.contains(e.relatedTarget as Node);
|
||||
if (isMovingOutside) {
|
||||
hovering.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative">
|
||||
<aside ref="sidenavRef" :class="[
|
||||
'h-full max-w-fit bg-[var(--color-base)] overflow-hidden will-change-width text-[var(--color-muted)] select-none',
|
||||
open ? 'w-full mr-2' : 'w-0 mr-0',
|
||||
isResizing ? '' : 'transition-[width,margin] duration-250 ease-[cubic-bezier(0,0.55,0.45,1)]'
|
||||
]" :style="open ? { width: `${sidebarWidth}px` } : {}" @mouseenter="hovering = true"
|
||||
@mouseleave="hovering = false" @focusin="hovering = true" @focusout="onFocusOut">
|
||||
'sidenav',
|
||||
open ? 'sidenav--open' : 'sidenav--closed',
|
||||
isResizing ? 'sidenav--resizing' : ''
|
||||
]" :style="open ? { width: `${sidebarWidth}px` } : {}" @mouseenter="isMouseOver = true"
|
||||
@mouseleave="isMouseOver = false" @focusin="isFocused = true" @focusout="onFocusOut">
|
||||
<div :style="{ minWidth: `${sidebarWidth}px` }" class="flex flex-col h-full justify-between">
|
||||
<div class="flex flex-col h-full max-h-full overflow-y-hidden">
|
||||
<!-- Header -->
|
||||
<div class="relative flex flex-row gap-2 justify-between items-center mb-1.5">
|
||||
|
||||
<SidenavHeader v-if="navKind === 'home'" v-model="hovering" />
|
||||
<SidenavHeaderAgent v-else-if="navKind === 'agent'" v-model="hovering" />
|
||||
<SidenavHeader v-if="navKind === 'home'" />
|
||||
<SidenavHeaderAgent v-else-if="navKind === 'agent'" />
|
||||
|
||||
<div class="flex items-center justify-end text-[var(--color-muted)] gap-0.5">
|
||||
<div ref="closeSidenavRef" style="width: 0;"
|
||||
<div :style="isHovering ? 'width: 32px;' : 'width: 0px;'"
|
||||
:class="['flex-shrink-0 overflow-hidden rounded-lg transition-all duration-150 ease-[cubic-bezier(0.5,_1,_0.89,_1)] transform-origin-center-right']">
|
||||
<button aria-label="close sidebar" @click="closeSidebar" :class="[
|
||||
'flex text-5 h-8 w-8 items-center justify-center hover:bg-[var(--color-highlight)] focus-visible:bg-[var(--color-highlight)] scale-100 bg-transparent transition-inherit',
|
||||
'flex text-5 h-8 w-8 items-center justify-center hover:bg-[var(--color-highlight)] focus-visible:bg-[var(--color-highlight)] bg-transparent transition-inherit',
|
||||
]">
|
||||
<Icon name="mynaui:panel-left-close"
|
||||
:class="['transition-inherit', hovering ? 'opacity-100 scale-100' : 'opacity-0 scale-95']" />
|
||||
:class="['transition-inherit transform-origin-right-center', isHovering ? 'opacity-100 scale-100' : 'opacity-0 scale-95']" />
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="navKind === 'agent'" class="flex-shrink-0 overflow-hidden rounded-lg">
|
||||
@@ -143,3 +161,31 @@ const onFocusOut = (e: FocusEvent) => {
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.sidenav {
|
||||
height: 100%;
|
||||
max-width: fit-content;
|
||||
background: var(--color-base);
|
||||
overflow: hidden;
|
||||
will-change: width;
|
||||
color: var(--color-muted);
|
||||
user-select: none;
|
||||
transition: width 250ms cubic-bezier(0, 0.55, 0.45, 1),
|
||||
margin 250ms cubic-bezier(0, 0.55, 0.45, 1);
|
||||
}
|
||||
|
||||
.sidenav--open {
|
||||
width: 100%;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.sidenav--closed {
|
||||
width: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.sidenav--resizing {
|
||||
transition: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -25,15 +25,18 @@ watch(() => props.checked, (newValue) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button role="switch" class="vl-toggle-switch" :aria-disabled="(props.disabled === true) ? 'true' : 'false'"
|
||||
:aria-label="label" :aria-labelledby="id" :tabindex="(disabled) ? '-1' : '0'" @click="(e) => $emit('click', e)"
|
||||
:aria-checked="active" :data-state="(active) ? 'checked' : 'unchecked'">
|
||||
<button :id="id" role="switch" class="vl-toggle-switch"
|
||||
:aria-disabled="(props.disabled === true) ? 'true' : 'false'" :aria-label="label" :aria-labelledby="id"
|
||||
:tabindex="(disabled) ? '-1' : '0'" @click="(e) => $emit('click', e)" :aria-checked="active"
|
||||
:data-state="(active) ? 'checked' : 'unchecked'">
|
||||
<div></div>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.vl-toggle-switch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: inherit;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
@@ -52,12 +55,14 @@ watch(() => props.checked, (newValue) => {
|
||||
}
|
||||
|
||||
.vl-toggle-switch div {
|
||||
transform-origin: center left;
|
||||
will-change: transform;
|
||||
position: relative;
|
||||
left: 0;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
background: #f7f7f7;
|
||||
border-radius: 90px;
|
||||
border-radius: 9999px;
|
||||
pointer-events: none;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
@@ -67,11 +72,15 @@ watch(() => props.checked, (newValue) => {
|
||||
}
|
||||
|
||||
.vl-toggle-switch[data-state="checked"] div {
|
||||
left: 100%;
|
||||
transform: translateX(-100%);
|
||||
transform-origin: center right;
|
||||
transform: translateX(calc(2.5em - 1em - 0.5rem));
|
||||
}
|
||||
|
||||
.vl-toggle-switch:active div {
|
||||
width: 1.3em;
|
||||
}
|
||||
|
||||
.vl-toggle-switch[data-state="checked"]:active div {
|
||||
transform: translateX(calc(2.5em - 1.3em - 0.5rem));
|
||||
}
|
||||
</style>
|
||||
@@ -1,14 +1,34 @@
|
||||
import type schema from "#triplit/schema";
|
||||
import type { Entity } from "@triplit/client";
|
||||
|
||||
export const useAgents = async () => {
|
||||
const triplit = useTriplitClient();
|
||||
|
||||
const { results: agents } = await useQuery('agents', triplit, triplit.query('agents').Include('topics'));
|
||||
const { results: agents, unsubscribe } = await useQuery('agents', triplit, triplit.query('agents').Include('topics'));
|
||||
|
||||
const getAgent = (id: string) => {
|
||||
return agents.value?.find((a) => a.id === id);
|
||||
const createAgent = async (): Promise<Readonly<Entity<typeof schema, 'agents'>> | null> => {
|
||||
const { user } = useAuth();
|
||||
if (!user.value) {
|
||||
console.error('No user');
|
||||
return null;
|
||||
}
|
||||
|
||||
return triplit.insert('agents', {
|
||||
name: 'New Agent',
|
||||
userId: user.value.id,
|
||||
systemPrompt: 'You are a helpful assistant.',
|
||||
defaultModelId: null,
|
||||
imageUrl: null,
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
agents,
|
||||
getAgent,
|
||||
unsubscribe,
|
||||
getAgent: (id: string) => {
|
||||
return agents.value?.find((a: any) => a.id === id);
|
||||
},
|
||||
createAgent,
|
||||
};
|
||||
};
|
||||
|
||||
+169
-51
@@ -1,66 +1,184 @@
|
||||
import type { BetterAuthClientOptions, InferSessionFromClient, InferUserFromClient } from 'better-auth/client';
|
||||
import { authClient } from '~~/lib/auth-client';
|
||||
import type { User, Session } from 'better-auth';
|
||||
import type { Result } from '~~/types/result';
|
||||
import { Ok, Err } from '~~/types/result';
|
||||
import { createAuthClient } from 'better-auth/vue';
|
||||
|
||||
export enum AuthError {
|
||||
NotAuthenticated = 'NOT_AUTHENTICATED',
|
||||
SignInFailed = 'SIGN_IN_FAILED',
|
||||
SignUpFailed = 'SIGN_UP_FAILED',
|
||||
SignOutFailed = 'SIGN_OUT_FAILED',
|
||||
NetworkError = 'NETWORK_ERROR',
|
||||
}
|
||||
|
||||
export interface AuthState {
|
||||
user: User | null;
|
||||
session: Session | null;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export const useAuth = () => {
|
||||
const session = useState<InferSessionFromClient<BetterAuthClientOptions> | null>('auth:session', () => null);
|
||||
const user = useState<InferUserFromClient<BetterAuthClientOptions> | null>('auth:user', () => null);
|
||||
const sessionFetching = import.meta.server ? ref(false) : useState('auth:sessionFetching', () => false);
|
||||
const url = useRequestURL();
|
||||
const headers = useRequestHeaders();
|
||||
const client = createAuthClient({
|
||||
baseURL: url.origin,
|
||||
fetchOptions: {
|
||||
headers
|
||||
}
|
||||
});
|
||||
|
||||
const fetchSession = async () => {
|
||||
if (sessionFetching.value) {
|
||||
console.log('already fetching session');
|
||||
return;
|
||||
const state = useState<AuthState>('auth:state', () => ({
|
||||
user: null,
|
||||
session: null,
|
||||
isLoading: false,
|
||||
}));
|
||||
|
||||
const isAuthenticated = computed(() => !!state.value.session);
|
||||
const userId = computed(() => state.value.user?.id ?? null);
|
||||
|
||||
const fetchSession = async (): Promise<Result<{ session: Session | null; user: User | null }, AuthError>> => {
|
||||
state.value.isLoading = true;
|
||||
|
||||
try {
|
||||
const { data } = await client.getSession();
|
||||
|
||||
if (data) {
|
||||
state.value.session = data.session;
|
||||
state.value.user = data.user;
|
||||
return Ok({ session: data.session, user: data.user });
|
||||
}
|
||||
|
||||
state.value.session = null;
|
||||
state.value.user = null;
|
||||
return Ok({ session: null, user: null });
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch session:', error);
|
||||
return Err(AuthError.NetworkError);
|
||||
} finally {
|
||||
state.value.isLoading = false;
|
||||
}
|
||||
sessionFetching.value = true;
|
||||
let data: {
|
||||
session: InferSessionFromClient<BetterAuthClientOptions>;
|
||||
user: InferUserFromClient<BetterAuthClientOptions>;
|
||||
} | null = null;
|
||||
if (import.meta.server) {
|
||||
data =
|
||||
(
|
||||
await useFetch<{
|
||||
session: InferSessionFromClient<BetterAuthClientOptions>;
|
||||
user: InferUserFromClient<BetterAuthClientOptions>;
|
||||
}>('/api/auth/get-session')
|
||||
).data.value ?? null;
|
||||
} else {
|
||||
data = (await authClient.getSession()).data;
|
||||
}
|
||||
session.value = data?.session || null;
|
||||
user.value = data?.user || null;
|
||||
sessionFetching.value = false;
|
||||
return data;
|
||||
};
|
||||
|
||||
if (import.meta.client) {
|
||||
authClient.$store.listen('$sessionSignal', async (signal) => {
|
||||
if (!signal) return;
|
||||
await fetchSession();
|
||||
const signIn = async (
|
||||
email: string,
|
||||
password: string
|
||||
): Promise<Result<{ user: User; token: string }, { error: AuthError, data?: any }>> => {
|
||||
state.value.isLoading = true;
|
||||
|
||||
if (!session.value) return;
|
||||
try {
|
||||
const { data, error } = await client.signIn.email({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error('Sign in failed:', error);
|
||||
return Err({ error: AuthError.SignInFailed, data: error });
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return Err({ error: AuthError.SignInFailed });
|
||||
}
|
||||
|
||||
state.value.user = data.user;
|
||||
|
||||
const triplit = useTriplitClient();
|
||||
if ('updateOptions' in triplit) {
|
||||
triplit.updateOptions({
|
||||
token: session.value.token,
|
||||
});
|
||||
if ('startSession' in triplit && data.token) {
|
||||
await triplit.startSession(data.token);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
clearNuxtData();
|
||||
|
||||
return Ok({ user: data.user, token: data.token });
|
||||
} catch (err) {
|
||||
console.error('Sign in error:', err);
|
||||
return Err({ error: AuthError.NetworkError });
|
||||
} finally {
|
||||
state.value.isLoading = false;
|
||||
}
|
||||
};
|
||||
|
||||
const signUp = async (
|
||||
email: string,
|
||||
password: string,
|
||||
name: string
|
||||
): Promise<Result<{ user: User; token: string }, { error: AuthError, data?: any }>> => {
|
||||
state.value.isLoading = true;
|
||||
|
||||
try {
|
||||
const { data, error } = await client.signUp.email({
|
||||
email,
|
||||
password,
|
||||
name,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error('Sign up failed:', error);
|
||||
return Err({ error: AuthError.SignUpFailed, data: error });
|
||||
}
|
||||
|
||||
if (!data || !data.token) {
|
||||
return Err({ error: AuthError.SignUpFailed });
|
||||
}
|
||||
|
||||
state.value.user = data.user;
|
||||
|
||||
const triplit = useTriplitClient();
|
||||
if ('startSession' in triplit) {
|
||||
await triplit.startSession(data.token);
|
||||
}
|
||||
|
||||
clearNuxtData();
|
||||
|
||||
return Ok({ user: data.user, token: data.token });
|
||||
} catch (err) {
|
||||
console.error('Sign up error:', err);
|
||||
return Err({ error: AuthError.NetworkError });
|
||||
} finally {
|
||||
state.value.isLoading = false;
|
||||
}
|
||||
};
|
||||
|
||||
const signOut = async (): Promise<Result<void, AuthError>> => {
|
||||
state.value.isLoading = true;
|
||||
|
||||
try {
|
||||
const { error } = await client.signOut();
|
||||
|
||||
if (error) {
|
||||
console.error('Sign out failed:', error);
|
||||
return Err(AuthError.SignOutFailed);
|
||||
}
|
||||
|
||||
state.value.user = null;
|
||||
state.value.session = null;
|
||||
|
||||
const triplit = useTriplitClient();
|
||||
if ('disconnect' in triplit) {
|
||||
triplit.disconnect();
|
||||
}
|
||||
|
||||
clearNuxtData();
|
||||
|
||||
return Ok(undefined);
|
||||
} catch (err) {
|
||||
console.error('Sign out error:', err);
|
||||
return Err(AuthError.NetworkError);
|
||||
} finally {
|
||||
state.value.isLoading = false;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
session,
|
||||
user,
|
||||
loggedIn: computed(() => !!session.value),
|
||||
signIn: authClient.signIn,
|
||||
signUp: authClient.signUp,
|
||||
async signOut() {
|
||||
await authClient.signOut();
|
||||
session.value = null;
|
||||
user.value = null;
|
||||
return navigateTo('/auth/login');
|
||||
},
|
||||
client,
|
||||
user: computed(() => state.value.user),
|
||||
session: computed(() => state.value.session),
|
||||
isLoading: computed(() => state.value.isLoading),
|
||||
isAuthenticated,
|
||||
userId,
|
||||
fetchSession,
|
||||
signIn,
|
||||
signUp,
|
||||
signOut,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,53 +1,68 @@
|
||||
import { ref, watch, onUnmounted, type Ref } from 'vue';
|
||||
|
||||
export function useAutoScroll(elementRef: Ref<HTMLElement | null>) {
|
||||
const userIsScrollingUp = ref(false);
|
||||
const THRESHOLD = 50;
|
||||
export function useAutoScroll(elementRef: Ref<HTMLElement | null>, options: {
|
||||
threshold?: number;
|
||||
} = {}) {
|
||||
const { threshold = 80 } = options;
|
||||
|
||||
const isAtBottom = () => {
|
||||
const el = elementRef.value;
|
||||
if (!el) return false;
|
||||
const distanceToBottom = el.scrollHeight - el.scrollTop - el.clientHeight;
|
||||
return distanceToBottom <= THRESHOLD;
|
||||
};
|
||||
const isUserScrollingUp = ref(false);
|
||||
const shouldAutoScroll = ref(true);
|
||||
|
||||
const scrollToBottom = (behavior: ScrollBehavior = 'auto') => {
|
||||
const scrollToBottom = (behavior: ScrollBehavior = 'smooth') => {
|
||||
const el = elementRef.value;
|
||||
if (!el) return;
|
||||
el.scrollTo({
|
||||
top: el.scrollHeight,
|
||||
behavior,
|
||||
});
|
||||
shouldAutoScroll.value = true;
|
||||
};
|
||||
|
||||
const handleScroll = () => {
|
||||
const el = elementRef.value;
|
||||
if (!el) return;
|
||||
|
||||
userIsScrollingUp.value = !isAtBottom();
|
||||
const { scrollTop, scrollHeight, clientHeight } = el;
|
||||
const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
|
||||
|
||||
if (distanceFromBottom <= threshold) {
|
||||
if (isUserScrollingUp.value) {
|
||||
isUserScrollingUp.value = false;
|
||||
shouldAutoScroll.value = true;
|
||||
}
|
||||
} else {
|
||||
isUserScrollingUp.value = true;
|
||||
shouldAutoScroll.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
let observer: MutationObserver | null = null;
|
||||
let timeout: NodeJS.Timeout | null = null;
|
||||
|
||||
watch(elementRef, (newEl, oldEl) => {
|
||||
if (oldEl) {
|
||||
oldEl.removeEventListener('scroll', handleScroll);
|
||||
observer?.disconnect();
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
timeout = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (newEl) {
|
||||
newEl.addEventListener('scroll', handleScroll, { passive: true });
|
||||
|
||||
observer = new MutationObserver(() => {
|
||||
if (!userIsScrollingUp.value) {
|
||||
scrollToBottom();
|
||||
// Only auto-scroll if user hasn't scrolled up and is near bottom
|
||||
if (!isUserScrollingUp.value && shouldAutoScroll.value) {
|
||||
scrollToBottom('instant');
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(newEl, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
characterData: true
|
||||
characterData: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -55,10 +70,13 @@ export function useAutoScroll(elementRef: Ref<HTMLElement | null>) {
|
||||
onUnmounted(() => {
|
||||
elementRef.value?.removeEventListener('scroll', handleScroll);
|
||||
observer?.disconnect();
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
userIsScrollingUp,
|
||||
scrollToBottom,
|
||||
isUserScrollingUp, // expose for UI feedback (optional)
|
||||
};
|
||||
}
|
||||
+258
-56
@@ -2,8 +2,32 @@ import type schema from "#triplit/schema";
|
||||
import type { Entity } from "@triplit/client";
|
||||
import type { ModelMessage } from "ai";
|
||||
import { decrypt, base64ToUint8Array } from "~/utils/crypto";
|
||||
import { type Result, Ok, Err } from "~~/types/result";
|
||||
import { assert } from "~~/utils/assert";
|
||||
|
||||
export type Message = Entity<typeof schema, 'messages'> & { parts: (Entity<typeof schema, 'message_parts'> & { toolCall: Entity<typeof schema, 'tool_calls'> | null } | undefined)[] };
|
||||
export type MessageEntity = Entity<typeof schema, 'messages'> & {
|
||||
parts: (Entity<typeof schema, 'message_parts'> & {
|
||||
toolCall: Entity<typeof schema, 'tool_calls'> | null
|
||||
})[] | undefined
|
||||
} & { generation: Entity<typeof schema, 'generations'> | null }
|
||||
|
||||
export type Message =
|
||||
MessageEntity & {
|
||||
children: (MessageEntity | undefined)[];
|
||||
};
|
||||
|
||||
export enum ChatErrorType {
|
||||
NoModel = 0,
|
||||
NoProvider,
|
||||
NoAgent,
|
||||
NoUser,
|
||||
DatabaseOperationFailed,
|
||||
GenerationFailed,
|
||||
MarshallFailed,
|
||||
NoProviderApiKey,
|
||||
NoMessage,
|
||||
Unimplemented,
|
||||
}
|
||||
|
||||
export const useChat = (agentId: string) => {
|
||||
const triplit = useTriplitClient();
|
||||
@@ -22,14 +46,13 @@ export const useChat = (agentId: string) => {
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
if ('flush' in triplit) {
|
||||
await triplit.flush();
|
||||
}
|
||||
assert('flush' in triplit);
|
||||
await triplit.flush();
|
||||
|
||||
return newTopic;
|
||||
};
|
||||
|
||||
const marshallMessages = (agent: Entity<typeof schema, 'agents'>, messages: Readonly<Message[]>) => {
|
||||
const marshallMessages = (agent: Entity<typeof schema, 'agents'>, messages: Readonly<MessageEntity[]>): Result<ModelMessage[], string> => {
|
||||
const marshalledMessages: ModelMessage[] = [];
|
||||
|
||||
if (agent && agent.systemPrompt) {
|
||||
@@ -49,8 +72,8 @@ export const useChat = (agentId: string) => {
|
||||
});
|
||||
break;
|
||||
case 'assistant':
|
||||
message.parts.forEach((part) => {
|
||||
if (!part) throw new Error('Part is undefined');
|
||||
(message.parts || []).forEach((part) => {
|
||||
if (!part) return Err('Part is undefined')
|
||||
|
||||
switch (part.type) {
|
||||
case 'text':
|
||||
@@ -62,22 +85,20 @@ export const useChat = (agentId: string) => {
|
||||
break;
|
||||
}
|
||||
case 'tool-call': {
|
||||
if (part.toolCall === null) throw new Error('Tool call is null');
|
||||
if (part.toolCall === null) return Err('Tool call is null')
|
||||
|
||||
if (part.toolCall.status === 'pending') {
|
||||
throw new Error(
|
||||
'Marshalling tool call that is still pending. This is likely a UI bug if this happens.',
|
||||
);
|
||||
return Err('Marshalling tool call that is still pending. This is likely a UI bug if this happens.')
|
||||
}
|
||||
|
||||
let inputValue: string = '';
|
||||
|
||||
switch (typeof part.toolCall.input!.value) {
|
||||
case 'string':
|
||||
switch (part.toolCall.input!.type) {
|
||||
case 'text':
|
||||
inputValue = part.toolCall.input!.value;
|
||||
break;
|
||||
case 'object':
|
||||
inputValue = JSON.stringify(part.toolCall.input!.value, null, 2);
|
||||
case 'json':
|
||||
inputValue = JSON.parse(part.toolCall.input!.value);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -152,39 +173,26 @@ export const useChat = (agentId: string) => {
|
||||
}
|
||||
} break;
|
||||
default:
|
||||
throw new Error(`Unknown part type: ${part.type}`);
|
||||
return Err(`Unknown part type: ${part.type}`)
|
||||
}
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown message role: ${message.role}`);
|
||||
return Err(`Unknown message role: ${message.role}`)
|
||||
}
|
||||
});
|
||||
|
||||
return marshalledMessages;
|
||||
return Ok(marshalledMessages);
|
||||
};
|
||||
|
||||
const sendMessage = async (
|
||||
message: string,
|
||||
const startGeneration = async (
|
||||
messages: ModelMessage[],
|
||||
args: Record<string, any>,
|
||||
topic: Entity<typeof schema, 'topics'>,
|
||||
topicMessages: Message[],
|
||||
agent: Entity<typeof schema, 'agents'>,
|
||||
provider: Entity<typeof schema, 'providers'>,
|
||||
model: Entity<typeof schema, 'models'>,
|
||||
) => {
|
||||
const newMessage = await triplit.insert('messages', {
|
||||
topicId: topic.id,
|
||||
createdAt: new Date().toISOString(),
|
||||
content: message,
|
||||
role: 'user',
|
||||
}) as Message;
|
||||
|
||||
const messages = marshallMessages(
|
||||
agent,
|
||||
topicMessages.concat(newMessage)
|
||||
);
|
||||
|
||||
|
||||
parentMessageId: string | null = null
|
||||
): Promise<Result<void, ChatErrorType>> => {
|
||||
let providerApiKey: string | undefined = undefined;
|
||||
if (provider.config.apiKey !== undefined) {
|
||||
const key = await crypto.subtle.importKey(
|
||||
@@ -201,32 +209,226 @@ export const useChat = (agentId: string) => {
|
||||
);
|
||||
}
|
||||
|
||||
return $fetch('/api/chat/generate', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
messages,
|
||||
topicId: topic.id,
|
||||
model: {
|
||||
providerId: provider.id,
|
||||
modelId: model.id,
|
||||
args: {
|
||||
temperature: 0.7,
|
||||
max_tokens: 100,
|
||||
top_p: 1,
|
||||
frequency_penalty: 0,
|
||||
presence_penalty: 0,
|
||||
try {
|
||||
$fetch('/api/chat/generate', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
messages,
|
||||
topicId: topic.id,
|
||||
parentMessageId,
|
||||
model: {
|
||||
providerId: provider.id,
|
||||
modelId: model.id,
|
||||
args,
|
||||
},
|
||||
providerApiKey: providerApiKey,
|
||||
},
|
||||
providerApiKey: providerApiKey,
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
return Ok(undefined);
|
||||
} catch (error) {
|
||||
console.error('Failed to generate:', error);
|
||||
return Err(ChatErrorType.GenerationFailed);
|
||||
}
|
||||
}
|
||||
|
||||
const sendMessage = async (
|
||||
message: string,
|
||||
topic: Entity<typeof schema, 'topics'>,
|
||||
topicMessages: MessageEntity[],
|
||||
agent: Entity<typeof schema, 'agents'>,
|
||||
provider: Entity<typeof schema, 'providers'>,
|
||||
model: Entity<typeof schema, 'models'>,
|
||||
): Promise<Result<void, ChatErrorType>> => {
|
||||
const { user } = useAuth();
|
||||
if (!user.value) {
|
||||
console.error('No user');
|
||||
return Err(ChatErrorType.NoUser);
|
||||
}
|
||||
|
||||
const newMessage = await triplit.insert('messages', {
|
||||
userId: user.value.id,
|
||||
topicId: topic.id,
|
||||
createdAt: new Date().toISOString(),
|
||||
content: message,
|
||||
role: 'user',
|
||||
}).catch(error => {
|
||||
console.error('Failed to insert message:', error);
|
||||
return Err(ChatErrorType.DatabaseOperationFailed);
|
||||
}) as Message;
|
||||
|
||||
const messages = marshallMessages(
|
||||
agent,
|
||||
topicMessages.concat(newMessage)
|
||||
);
|
||||
|
||||
if (messages.ok === false) {
|
||||
console.error('Failed to marshall messages:', messages.error);
|
||||
return Err(ChatErrorType.MarshallFailed)
|
||||
}
|
||||
|
||||
const args = {
|
||||
temperature: 1,
|
||||
max_tokens: 100,
|
||||
top_p: 1,
|
||||
frequency_penalty: 0,
|
||||
presence_penalty: 0,
|
||||
};
|
||||
|
||||
return startGeneration(messages.data, args, topic, provider, model)
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param messageId The ID of the message we wish to regenerate *for*, this
|
||||
* can be either a user's message or an agent's message, and we will find
|
||||
* the message that should be regenerated automatically
|
||||
* @param topic
|
||||
* @param topicMessages messages in the current topic, if this does not
|
||||
* include an agent message after the user's message we select, the new
|
||||
* message will not be a child of the previous message. However, if this
|
||||
* array contains an agent message that is to be regenerated, the new
|
||||
* message will have that message's id as its parent. The message
|
||||
* reference by messageId *must* be in this array
|
||||
* @param agent
|
||||
* @param provider
|
||||
* @param model
|
||||
*/
|
||||
const regenerateMessage = async (
|
||||
messageId: string,
|
||||
topic: Entity<typeof schema, 'topics'>,
|
||||
topicMessages: MessageEntity[],
|
||||
agent: Entity<typeof schema, 'agents'>,
|
||||
provider: Entity<typeof schema, 'providers'>,
|
||||
model: Entity<typeof schema, 'models'>
|
||||
): Promise<Result<void, ChatErrorType>> => {
|
||||
const targetMessage = topicMessages.find(message => message.id === messageId);
|
||||
if (!targetMessage) {
|
||||
return Err(ChatErrorType.NoMessage);
|
||||
}
|
||||
let targetMessageIndex = topicMessages.indexOf(targetMessage);
|
||||
|
||||
const args = {
|
||||
temperature: 1,
|
||||
max_tokens: 100,
|
||||
top_p: 1,
|
||||
frequency_penalty: 0,
|
||||
presence_penalty: 0,
|
||||
};
|
||||
|
||||
let parentMessageId = null;
|
||||
let focusedMessages;
|
||||
if (targetMessage.role === 'user') {
|
||||
// we need to find the next agent message
|
||||
while (targetMessageIndex < topicMessages.length) {
|
||||
const currentMessage = topicMessages[targetMessageIndex];
|
||||
if (!currentMessage) break;
|
||||
|
||||
if (currentMessage.role === 'assistant') {
|
||||
parentMessageId = currentMessage.parentMessageId || currentMessage.id;
|
||||
focusedMessages = topicMessages.slice(0, targetMessageIndex).filter(message => message.id !== currentMessage.id);
|
||||
break;
|
||||
}
|
||||
targetMessageIndex++;
|
||||
}
|
||||
} else {
|
||||
parentMessageId = targetMessage.parentMessageId || topicMessages[targetMessageIndex]!.id;
|
||||
focusedMessages = topicMessages.slice(0, targetMessageIndex).filter(message => message.id !== messageId);
|
||||
}
|
||||
|
||||
if (focusedMessages === undefined) {
|
||||
focusedMessages = topicMessages;
|
||||
}
|
||||
|
||||
if (parentMessageId === null) {
|
||||
const messages = marshallMessages(
|
||||
agent,
|
||||
topicMessages
|
||||
);
|
||||
if (messages.ok === false) {
|
||||
console.error('Failed to marshall messages:', messages.error);
|
||||
return Err(ChatErrorType.MarshallFailed)
|
||||
}
|
||||
|
||||
return startGeneration(messages.data, args, topic, provider, model);
|
||||
}
|
||||
|
||||
const focusedMessageIndex = topicMessages.findIndex(m => m.id === parentMessageId);
|
||||
if (focusedMessageIndex !== topicMessages.length - 1) {
|
||||
|
||||
}
|
||||
|
||||
const messages = marshallMessages(
|
||||
agent,
|
||||
focusedMessages
|
||||
);
|
||||
if (messages.ok === false) {
|
||||
console.error('Failed to marshall messages:', messages.error);
|
||||
return Err(ChatErrorType.MarshallFailed)
|
||||
}
|
||||
|
||||
return startGeneration(messages.data, args, topic, provider, model, parentMessageId);
|
||||
}
|
||||
|
||||
const autoRename = async (topicId: string, prompt: string) => {
|
||||
const { settings } = await useUserSettings();
|
||||
|
||||
console.log(settings.value);
|
||||
|
||||
if (!settings.value.systemAssistants.rename.enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!settings.value.systemAssistants.rename.modelId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await triplit.update('topics', topicId, {
|
||||
renaming: true
|
||||
});
|
||||
|
||||
const model = await triplit.fetchOne(triplit.query('models').Where('id', '=', settings.value.systemAssistants.rename.modelId).Include('provider'));
|
||||
|
||||
if (!model) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let providerApiKey: string | undefined = undefined;
|
||||
if (model.provider!.config.apiKey !== undefined) {
|
||||
const key = await crypto.subtle.importKey(
|
||||
"jwk",
|
||||
JSON.parse(window.localStorage.getItem("encryptionKey")!),
|
||||
"AES-GCM",
|
||||
false,
|
||||
["encrypt", "decrypt"]
|
||||
)
|
||||
|
||||
providerApiKey = await decrypt(
|
||||
key,
|
||||
base64ToUint8Array(model.provider!.config.apiKey)
|
||||
);
|
||||
}
|
||||
|
||||
await $fetch(`/api/topic/auto-rename`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
modelId: model.id,
|
||||
topicId,
|
||||
prompt,
|
||||
providerApiKey,
|
||||
}),
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return {
|
||||
sendMessage,
|
||||
autoRename,
|
||||
regenerateMessage,
|
||||
createTopic,
|
||||
};
|
||||
}
|
||||
@@ -10,42 +10,57 @@ export type ProviderWithModels = Entity<typeof schema, 'providers'> & {
|
||||
};
|
||||
|
||||
export const useModels = async () => {
|
||||
const triplit = useTriplitClient();
|
||||
const nuxtApp = useNuxtApp() as any;
|
||||
|
||||
const providersQuery = triplit
|
||||
.query('providers')
|
||||
.Include('models')
|
||||
if (!nuxtApp._modelsSubscription) {
|
||||
if (!nuxtApp._modelsPromise) {
|
||||
const triplit = useTriplitClient();
|
||||
|
||||
const { results: providers, unsubscribe } = await useQuery('providers', triplit, providersQuery);
|
||||
const providersQuery = triplit
|
||||
.query('providers')
|
||||
.Include('models');
|
||||
|
||||
console.log("GET PROVIDERS", providers.value);
|
||||
nuxtApp._modelsPromise = useQuery('providers', triplit, providersQuery).then((sub) => {
|
||||
nuxtApp._modelsSubscription = sub;
|
||||
return sub;
|
||||
});
|
||||
}
|
||||
await nuxtApp._modelsPromise;
|
||||
}
|
||||
|
||||
// const enabledProvidersWithModels = computed<ProviderWithModels[]>(() => {
|
||||
// if (!providers.value) return [];
|
||||
const { results: providers } = nuxtApp._modelsSubscription;
|
||||
|
||||
// return (providers.value as unknown as ProviderWithModels[]).filter(
|
||||
// (provider: ProviderWithModels) => provider.models && provider.models.length > 0
|
||||
// );
|
||||
// });
|
||||
if (!nuxtApp._allModels) {
|
||||
nuxtApp._allModels = computed<ModelWithProvider[]>(() => {
|
||||
if (!providers.value) return [];
|
||||
|
||||
const allEnabledModels = computed<ModelWithProvider[]>(() => {
|
||||
return providers.value?.flatMap((provider) =>
|
||||
provider.models.map((model) => ({
|
||||
...model,
|
||||
provider,
|
||||
}))
|
||||
);
|
||||
});
|
||||
const list: ModelWithProvider[] = [];
|
||||
|
||||
for (const provider of (providers.value as ProviderWithModels[])) {
|
||||
if (!provider.enabled) continue;
|
||||
|
||||
for (const model of provider.models) {
|
||||
if (!model.enabled) continue;
|
||||
|
||||
list.push({
|
||||
...model,
|
||||
provider
|
||||
} as unknown as ModelWithProvider);
|
||||
}
|
||||
}
|
||||
return list;
|
||||
});
|
||||
}
|
||||
|
||||
const getFirstAvailableModel = (): ModelWithProvider | null => {
|
||||
if (allEnabledModels.value.length === 0) return null;
|
||||
return allEnabledModels.value[0]!;
|
||||
if (nuxtApp._allModels.value.length === 0) return null;
|
||||
return nuxtApp._allModels.value[0]!;
|
||||
};
|
||||
|
||||
return {
|
||||
providers,
|
||||
allModels: allEnabledModels,
|
||||
providers: providers as Ref<ProviderWithModels[]>,
|
||||
allModels: nuxtApp._allModels as ComputedRef<ModelWithProvider[]>,
|
||||
getFirstAvailableModel,
|
||||
unsubscribe,
|
||||
unsubscribe: () => { },
|
||||
};
|
||||
};
|
||||
|
||||
@@ -8,6 +8,8 @@ export const useSettings = () => {
|
||||
};
|
||||
|
||||
const setPage = (id: string, params?: string | string[]) => {
|
||||
if (open.value === false) toggle();
|
||||
|
||||
currentPage.value = id;
|
||||
if (params) {
|
||||
if (typeof params === 'string') {
|
||||
|
||||
@@ -38,7 +38,7 @@ export const useSidebar = () => {
|
||||
};
|
||||
|
||||
return {
|
||||
open,
|
||||
open: readonly(open),
|
||||
toggle,
|
||||
close,
|
||||
openSidebar,
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
const SIDENAV_CONTEXT_KEY = Symbol('sidenav-context');
|
||||
|
||||
export interface SidenavContext {
|
||||
isHovered: Readonly<Ref<boolean>>;
|
||||
sidebarWidth: Readonly<Ref<number>>;
|
||||
isOpen: Readonly<Ref<boolean>>;
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
export const provideSidenavContext = (context: SidenavContext) => {
|
||||
provide(SIDENAV_CONTEXT_KEY, context);
|
||||
};
|
||||
|
||||
export const useSidenavContext = (): SidenavContext => {
|
||||
const context = inject<SidenavContext>(SIDENAV_CONTEXT_KEY);
|
||||
if (!context) {
|
||||
throw new Error('useSidenavContext must be used within a Sidenav provider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
export const useUserSettings = async () => {
|
||||
const user = useAuth().user;
|
||||
const triplit = useTriplitClient();
|
||||
|
||||
const { results: settings, unsubscribe } = await useQuery('settings', triplit, triplit.query('settings').Where('userId', '=', user.value!.id));
|
||||
|
||||
return { settings: computed(() => settings.value![0]!), unsubscribe };
|
||||
}
|
||||
@@ -1,19 +1,29 @@
|
||||
export default defineNuxtRouteMiddleware(async (to) => {
|
||||
const { session, fetchSession } = useAuth();
|
||||
const { session: rawSession, fetchSession } = useAuth();
|
||||
let session = rawSession.value;
|
||||
|
||||
if (!session.value) {
|
||||
await fetchSession();
|
||||
}
|
||||
const isAuthPage = to.path.toLowerCase().includes("/auth/");
|
||||
|
||||
const loggedIn = computed(() => !!session.value);
|
||||
// Only fetch session if not on auth pages (prevents race condition after logout)
|
||||
if (!rawSession.value && !isAuthPage) {
|
||||
const result = await fetchSession();
|
||||
if (result.ok) {
|
||||
session = result.data.session;
|
||||
} else {
|
||||
console.error("Failed to fetch session:", result.error);
|
||||
session = null;
|
||||
}
|
||||
}
|
||||
|
||||
// if authenticated, and on a signin/signup page, redirect to home page
|
||||
if (to.path.toLowerCase().includes("/auth/") && loggedIn.value) {
|
||||
return await navigateTo((to.query.to as string) ?? "/");
|
||||
}
|
||||
const loggedIn = computed(() => !!session);
|
||||
|
||||
// If not authenticated, and not on a signin/signup page, redirect to login page
|
||||
if (!loggedIn.value && !to.path.toLowerCase().includes("/auth/")) {
|
||||
return await navigateTo(`/auth/login?to=${to.path}`);
|
||||
}
|
||||
// if authenticated, and on a signin/signup page, redirect to home page
|
||||
if (isAuthPage && loggedIn.value) {
|
||||
return await navigateTo((to.query.to as string) ?? "/");
|
||||
}
|
||||
|
||||
// If not authenticated, and not on a signin/signup page, redirect to login page
|
||||
if (!loggedIn.value && !isAuthPage) {
|
||||
return await navigateTo(`/auth/login?to=${to.path}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import type { ModelWithProvider } from '~/composables/useModels';
|
||||
import type { ModelWithProvider, ProviderWithModels } from '~/composables/useModels';
|
||||
|
||||
const route = useRoute();
|
||||
const pendingMessage = ref<Message | null>(null);
|
||||
|
||||
const { createTopic, sendMessage } = useChat(route.params.id as string);
|
||||
const { getAgent } = await useAgents();
|
||||
const { createTopic, sendMessage, autoRename } = useChat(route.params.id as string);
|
||||
const { getAgent, unsubscribe: unsubscribeAgents } = await useAgents();
|
||||
const { providers, unsubscribe: unsubscribeModels } = await useModels();
|
||||
|
||||
const agent = computed(() => {
|
||||
@@ -15,22 +16,49 @@ const agent = computed(() => {
|
||||
return getAgent(route.params.id)!;
|
||||
});
|
||||
|
||||
if (!agent.value) navigateTo('/');
|
||||
|
||||
const handleSubmit = async (message: string, model: ModelWithProvider | null) => {
|
||||
if (!model) {
|
||||
console.error('No model selected');
|
||||
return;
|
||||
}
|
||||
|
||||
const user = useAuth().user;
|
||||
if (!user) {
|
||||
console.error('No user');
|
||||
return;
|
||||
}
|
||||
|
||||
pendingMessage.value = {
|
||||
id: '',
|
||||
userId: user.value!.id,
|
||||
topicId: null,
|
||||
content: message,
|
||||
role: 'user',
|
||||
parts: [],
|
||||
generation: null,
|
||||
parentMessageId: null,
|
||||
children: [],
|
||||
generationId: null,
|
||||
focusedIndex: null,
|
||||
deleted: false,
|
||||
createdAt: new Date(),
|
||||
}
|
||||
|
||||
const topic = await createTopic();
|
||||
if (!topic) throw new Error('Failed to create topic');
|
||||
|
||||
sendMessage(message, topic, [], agent.value!, model.provider, model);
|
||||
autoRename(topic.id, message);
|
||||
|
||||
return navigateTo(`/agent/${route.params.id}/topic/${topic.id}`);
|
||||
await navigateTo(`/agent/${route.params.id}/topic/${topic.id}`);
|
||||
|
||||
return sendMessage(message, topic, [], agent.value!, model.provider, model);
|
||||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
unsubscribeModels?.()
|
||||
unsubscribeAgents?.();
|
||||
unsubscribeModels?.();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -41,16 +69,22 @@ onUnmounted(() => {
|
||||
<div class="flex flex-col w-full px-4 overflow-y-auto h-full"
|
||||
style="scrollbar-width: thin; scrollbar-color: #888 transparent;" ref="chatPane">
|
||||
<div class="flex-grow w-full flex justify-center">
|
||||
<div class="flex h-full max-w-4xl w-full flex-col gap-2 justify-end">
|
||||
<h1 v-if="agent" class="font-bold">{{ agent.name }}</h1>
|
||||
<p class="mb-28 text-[var(--color-muted)]">Select a topic to continue or create a new one</p>
|
||||
<div class="flex h-full max-w-4xl w-full flex-col gap-2"
|
||||
:class="pendingMessage === null ? 'justify-end' : ''">
|
||||
<div v-if="pendingMessage === null">
|
||||
<h1 v-if="agent" class="font-bold">{{ agent.name }}</h1>
|
||||
<p class="mb-28 text-[var(--color-muted)]">Select a topic to continue or create a new one</p>
|
||||
</div>
|
||||
<div class="opacity-70" v-else>
|
||||
<Message :message="pendingMessage" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sticky max-h-full z-10 bottom-0 w-full flex justify-center">
|
||||
<div class="pb-4 w-full max-w-4xl bg-[var(--color-neutral)] rounded-t-2xl">
|
||||
<ChatInput class="[view-transition-name:chat-prompt] duration-150 ease-in-out" :agent="agent"
|
||||
:providers="providers" @submit="handleSubmit"></ChatInput>
|
||||
:providers="providers.filter(p => p.enabled)" @submit="handleSubmit"></ChatInput>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
const { getAgent } = await useAgents();
|
||||
const { getAgent, unsubscribe: unsubscribeAgents } = await useAgents();
|
||||
const triplit = useTriplitClient();
|
||||
|
||||
const route = useRoute();
|
||||
@@ -35,6 +35,10 @@ const changeSystemPrompt = async (e: Event) => {
|
||||
systemPrompt: target.value,
|
||||
});
|
||||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
unsubscribeAgents?.();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -48,8 +52,9 @@ const changeSystemPrompt = async (e: Event) => {
|
||||
class="placeholder:text-[var(--color-highlight)] w-full bg-transparent rounded-none border-b-4 border-b-[var(--color-highlight-high)] text-12 p-0"
|
||||
type="text" :value="agent?.name" />
|
||||
</div>
|
||||
<div class="flex items-center gap-2 w-full h-full mb-14">
|
||||
<textarea placeholder="System Message..."
|
||||
<div class="flex flex-col gap-2 w-full h-full mb-14">
|
||||
<label class="text-sm text-[var(--color-text-subtle)]">System Message</label>
|
||||
<textarea placeholder="You are a helpful assistant."
|
||||
class="p-4 w-full h-full resize-none bg-transparent rounded-lg border border-[var(--color-highlight)]"
|
||||
:value="agent?.systemPrompt" @input="changeSystemPrompt"></textarea>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import type { Message } from '~/composables/useChat';
|
||||
import type { ModelWithProvider } from '~/composables/useModels';
|
||||
import type { Message, MessageEntity } from '~/composables/useChat';
|
||||
import type { ModelWithProvider, ProviderWithModels } from '~/composables/useModels';
|
||||
|
||||
const triplit = useTriplitClient();
|
||||
|
||||
const chatPane = ref<HTMLElement | null>(null);
|
||||
const route = useRoute();
|
||||
const { sendMessage } = useChat(route.params.id as string);
|
||||
const { getAgent } = await useAgents();
|
||||
const { providers, unsubscribe: unsubscribeModels } = await useModels();
|
||||
const { sendMessage, regenerateMessage } = useChat(route.params.id as string);
|
||||
const { getAgent, unsubscribe: unsubscribeAgents } = await useAgents();
|
||||
const { providers, unsubscribe: unsubscribeModels, allModels } = await useModels();
|
||||
|
||||
const agent = computed(() => {
|
||||
if (route.params.id === null || typeof route.params.id !== 'string') {
|
||||
@@ -22,39 +22,90 @@ const topicQuery = computed(() =>
|
||||
triplit
|
||||
.query('topics')
|
||||
.Where(['id', '=', route.params.topicId])
|
||||
.Include('generations')
|
||||
.Include('messages', (rel) =>
|
||||
rel('messages')
|
||||
.Include('generation')
|
||||
.Include('parts', (rel) => rel('parts').Include('toolCall')),
|
||||
)
|
||||
.Limit(1)
|
||||
);
|
||||
|
||||
const { results, unsubscribe: unsubscribeTopic } = await useQuery('topic', triplit, topicQuery);
|
||||
const messagesQuery = computed(() =>
|
||||
triplit
|
||||
.query('messages')
|
||||
.Where(['topicId', '=', route.params.topicId])
|
||||
.Order('createdAt', 'ASC')
|
||||
);
|
||||
|
||||
const partsQuery = computed(() =>
|
||||
triplit
|
||||
.query('message_parts')
|
||||
.Where(['topicId', '=', route.params.topicId])
|
||||
.Order('createdAt', 'ASC')
|
||||
.Include('toolCall')
|
||||
);
|
||||
|
||||
const generationsQuery = computed(() =>
|
||||
triplit
|
||||
.query('generations')
|
||||
.Where(['topicId', '=', route.params.topicId])
|
||||
);
|
||||
|
||||
const [
|
||||
{ results: rawTopic, unsubscribe: unsubscribeTopic },
|
||||
{ results: rawMessages, unsubscribe: unsubscribeMessages },
|
||||
{ results: rawParts, unsubscribe: unsubscribeParts },
|
||||
{ results: rawGenerations, unsubscribe: unsubscribeGenerations }
|
||||
] = await Promise.all([
|
||||
useQuery('topic', triplit, topicQuery),
|
||||
useQuery('messages', triplit, messagesQuery),
|
||||
useQuery('parts', triplit, partsQuery),
|
||||
useQuery('generations', triplit, generationsQuery),
|
||||
]);
|
||||
|
||||
const topic = computed(() => {
|
||||
if (results.value?.length === 0) return null;
|
||||
if (!rawMessages.value || !rawTopic.value || !rawTopic.value[0]) return null;
|
||||
|
||||
// copy messages to a mutable object and sort by createdAt
|
||||
const messages = results!.value![0]!.messages.map((message) => ({
|
||||
...message,
|
||||
parts: message.parts.map((part) => ({
|
||||
...part,
|
||||
toolCall: part.toolCall ? { ...part.toolCall } : null,
|
||||
})),
|
||||
}));
|
||||
messages.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
|
||||
// Build the messages tree manually for maximum performance
|
||||
const messagesMap = new Map();
|
||||
|
||||
// for each message, sort parts by createdAt
|
||||
messages.forEach((message) => {
|
||||
message.parts = message.parts
|
||||
.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime())
|
||||
.filter((part) => part.content !== '' || part.toolCall !== null);
|
||||
});
|
||||
return { ...results.value![0]!, messages };
|
||||
// First pass: Create message objects with parts arrays
|
||||
for (const msg of rawMessages.value) {
|
||||
messagesMap.set(msg.id, {
|
||||
...msg,
|
||||
parts: [],
|
||||
children: [],
|
||||
generation: rawGenerations.value?.find(g => g.id === msg.generationId) ?? null
|
||||
});
|
||||
}
|
||||
|
||||
// Second pass: Attach parts to messages
|
||||
if (rawParts.value) {
|
||||
for (const part of rawParts.value) {
|
||||
const msg = messagesMap.get(part.messageId);
|
||||
if (msg) {
|
||||
// Filter empty parts here if needed, or just push
|
||||
if (part.content !== '' || part.toolCall !== null) {
|
||||
msg.parts.push(part);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Third pass: Build children relationships
|
||||
const rootMessages = [];
|
||||
for (const msg of messagesMap.values()) {
|
||||
if (msg.parentMessageId && messagesMap.has(msg.parentMessageId)) {
|
||||
messagesMap.get(msg.parentMessageId).children.push(msg);
|
||||
} else {
|
||||
rootMessages.push(msg);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...rawTopic.value[0],
|
||||
messages: rootMessages as Message[],
|
||||
generations: rawGenerations.value || []
|
||||
};
|
||||
});
|
||||
|
||||
if (!topic.value) navigateTo(`/agent/${route.params.id}`);
|
||||
|
||||
const activeGeneration = computed(() => {
|
||||
if (topic.value === null) return null;
|
||||
return topic.value?.generations?.find((generation) => generation.status === 'pending') ?? null;
|
||||
@@ -78,12 +129,137 @@ const handleSubmit = async (message: string, model: ModelWithProvider | null) =>
|
||||
return;
|
||||
}
|
||||
|
||||
await sendMessage(message, topic.value!, topic.value!.messages as unknown as Message[], agent.value!, model.provider, model);
|
||||
const res = await sendMessage(message, topic.value!, focusedMessageTree.value, agent.value!, model.provider, model);
|
||||
if (!res.ok) {
|
||||
console.error('Failed to send message:', res.error);
|
||||
return;
|
||||
}
|
||||
|
||||
scrollToBottom('instant');
|
||||
};
|
||||
|
||||
const focusedMessageTree = computed(() => {
|
||||
const tree: MessageEntity[] = [];
|
||||
for (const message of topic.value?.messages || []) {
|
||||
if (message.focusedIndex !== undefined && message.focusedIndex !== null) {
|
||||
if (message.focusedIndex === 0) {
|
||||
tree.push(message);
|
||||
continue;
|
||||
}
|
||||
|
||||
tree.push(message.children[message.focusedIndex - 1]!);
|
||||
} else {
|
||||
tree.push(message);
|
||||
}
|
||||
}
|
||||
return tree;
|
||||
})
|
||||
|
||||
const handleRegenerate = async (message: Message) => {
|
||||
if (!agent.value.defaultModelId) {
|
||||
console.error('No model selected');
|
||||
return;
|
||||
}
|
||||
|
||||
let messageId;
|
||||
if (
|
||||
(message.focusedIndex !== undefined && message.focusedIndex !== null)
|
||||
&& message.focusedIndex > 0
|
||||
&& message.children.length > 0
|
||||
) {
|
||||
messageId = message.children[message.focusedIndex - 1]!.id;
|
||||
} else {
|
||||
messageId = message.id;
|
||||
}
|
||||
|
||||
const model = allModels.value.find(m => m.id === agent.value.defaultModelId);
|
||||
if (!model) {
|
||||
console.error('Model not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await regenerateMessage(messageId, topic.value!, focusedMessageTree.value, agent.value!, model.provider, model);
|
||||
if (!res.ok) {
|
||||
console.error('Failed to regenerate message:', ChatErrorType[res.error]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const deeplyDeleteMessage = async (message: MessageEntity) => {
|
||||
triplit.delete('messages', message.id);
|
||||
|
||||
if (message.generationId !== null && message.generationId !== undefined) {
|
||||
triplit.delete('generations', message.generationId);
|
||||
}
|
||||
|
||||
for (const part of message.parts || []) {
|
||||
triplit.delete('message_parts', part.id);
|
||||
}
|
||||
|
||||
if (topic.value?.messages.filter(m => m.id !== message.id).length === 0) {
|
||||
triplit.delete('topics', topic.value!.id);
|
||||
return navigateTo(`/agent/${route.params.id}/`);
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (rootMessage: Message) => {
|
||||
if (rootMessage.role === 'user') {
|
||||
deeplyDeleteMessage(rootMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
if (rootMessage.deleted === true) {
|
||||
const message = rootMessage.children[rootMessage.focusedIndex!];
|
||||
if (!message) {
|
||||
console.error('Message not found');
|
||||
return;
|
||||
}
|
||||
|
||||
deeplyDeleteMessage(message);
|
||||
|
||||
if (rootMessage.children.filter(child => child!.id !== message.id).length === 0) {
|
||||
deeplyDeleteMessage(rootMessage);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// - If the message has children, check if they are all soft deleted
|
||||
// - If they are all soft deleted, delete the message
|
||||
// - If they are not all soft deleted, mark only this message as deleted
|
||||
if (
|
||||
(rootMessage.focusedIndex !== undefined && rootMessage.focusedIndex !== null)
|
||||
&& rootMessage.focusedIndex > 0
|
||||
&& rootMessage.children.length > 0
|
||||
) {
|
||||
// we are a child message
|
||||
const message = rootMessage.children[rootMessage.focusedIndex - 1]!;
|
||||
if (!message) {
|
||||
console.error('Message not found');
|
||||
return;
|
||||
}
|
||||
|
||||
deeplyDeleteMessage(message);
|
||||
return;
|
||||
}
|
||||
|
||||
// we have no children
|
||||
if (rootMessage.children.length === 0) {
|
||||
deeplyDeleteMessage(rootMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// we are a root message and we have at least one living child, soft delete
|
||||
await triplit.update('messages', rootMessage.id, {
|
||||
deleted: true
|
||||
});
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
unsubscribeTopic?.();
|
||||
unsubscribeAgents?.();
|
||||
unsubscribeMessages?.();
|
||||
unsubscribeParts?.();
|
||||
unsubscribeGenerations?.();
|
||||
unsubscribeModels?.();
|
||||
});
|
||||
</script>
|
||||
@@ -96,15 +272,17 @@ onUnmounted(() => {
|
||||
<div class="flex-grow w-full flex justify-center">
|
||||
<div class="max-w-4xl w-full flex flex-col gap-2 pb-9"
|
||||
v-if="Array.isArray(topic?.messages) && topic.messages.length > 0">
|
||||
<Message v-for="message in topic.messages" :key="message.id" :message="message" />
|
||||
<Message v-for="message in topic.messages" @delete="handleDelete(message)" :key="message.id"
|
||||
@regenerate="handleRegenerate(message)" :message="message" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sticky max-h-full z-10 bottom-0 w-full flex justify-center">
|
||||
<div class="pb-4 w-full max-w-4xl bg-[var(--color-neutral)] rounded-t-2xl">
|
||||
<ChatInput class="[view-transition-name:chat-prompt] duration-150 ease-in-out"
|
||||
:loading="activeGeneration !== null" :agent="agent" :providers="providers"
|
||||
@submit="handleSubmit" @cancel="handleCancel"></ChatInput>
|
||||
:loading="activeGeneration !== null" :agent="agent"
|
||||
:providers="providers?.filter(p => p.enabled)" @submit="handleSubmit" @cancel="handleCancel">
|
||||
</ChatInput>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+19
-22
@@ -1,6 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { authClient } from '~~/lib/auth-client';
|
||||
import { deriveKey } from '~/utils/crypto';
|
||||
import { initSettings } from '~/utils/settings';
|
||||
|
||||
const { client: authClient, signIn } = useAuth();
|
||||
|
||||
definePageMeta({
|
||||
layout: 'auth',
|
||||
@@ -56,49 +58,44 @@ const submit = async () => {
|
||||
|
||||
loading.value = true;
|
||||
|
||||
const { data, error } = await authClient.signIn.email({
|
||||
email: form.email,
|
||||
password: form.password,
|
||||
});
|
||||
|
||||
const res = await signIn(form.email, form.password);
|
||||
loading.value = false;
|
||||
|
||||
if (error) {
|
||||
const errorCode = error.code! as keyof typeof authClient.$ERROR_CODES;
|
||||
if (!res.ok) {
|
||||
if (!res.error.data) {
|
||||
console.error('Failed to sign in:', res.error);
|
||||
alert('Something went wrong');
|
||||
return;
|
||||
}
|
||||
|
||||
const errorCode = res.error.data.code as keyof typeof authClient.$ERROR_CODES;
|
||||
|
||||
// TODO: i18n
|
||||
// ref https://www.better-auth.com/docs/concepts/client#error-codes
|
||||
switch (errorCode) {
|
||||
case 'INVALID_PASSWORD':
|
||||
passwordInputEl.value!.setCustomValidity(error.message!);
|
||||
passwordInputEl.value!.setCustomValidity(res.error.data.message!);
|
||||
passwordInputEl.value!.reportValidity();
|
||||
break;
|
||||
case 'ACCOUNT_NOT_FOUND':
|
||||
case 'USER_NOT_FOUND':
|
||||
case 'USER_EMAIL_NOT_FOUND':
|
||||
emailInputEl.value!.setCustomValidity(error.message!);
|
||||
emailInputEl.value!.setCustomValidity(res.error.data.message!);
|
||||
emailInputEl.value!.reportValidity();
|
||||
break;
|
||||
default:
|
||||
console.log(error);
|
||||
alert(`Something went wrong. ${error.message}`);
|
||||
console.log(res.error);
|
||||
alert(`Something went wrong. ${res.error.data.message}`);
|
||||
break;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const key = await deriveKey(form.password, data.user.id);
|
||||
const key = await deriveKey(form.password, res.data.user.id);
|
||||
localStorage.setItem('encryptionKey', JSON.stringify(key));
|
||||
|
||||
// force a session refetch
|
||||
clearNuxtData();
|
||||
|
||||
// success
|
||||
const triplit = useTriplitClient();
|
||||
if ('startSession' in triplit) {
|
||||
await triplit.startSession(data.token);
|
||||
}
|
||||
initSettings(res.data.user.id);
|
||||
|
||||
return navigateTo(to ?? '/');
|
||||
};
|
||||
@@ -114,7 +111,7 @@ const submit = async () => {
|
||||
<input required minlength="8" maxlength="128" ref="passwordInputEl" type="password"
|
||||
autocomplete="current-password" id="password" v-model="form.password" />
|
||||
<button :disabled="!hydrated" class="accent" type="submit">
|
||||
<Icon v-if="loading" class="text-6" name="svg-spinners:90-ring-with-bg" />
|
||||
<Icon v-if="loading" class="text-6 h-6" name="svg-spinners:90-ring-with-bg" />
|
||||
<span v-else>Login</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
+16
-26
@@ -1,12 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { authClient } from '~~/lib/auth-client';
|
||||
import { deriveKey } from '~/utils/crypto';
|
||||
const { client: authClient, signUp, session } = useAuth();
|
||||
|
||||
definePageMeta({
|
||||
layout: 'auth',
|
||||
});
|
||||
|
||||
const { session } = await useAuth();
|
||||
const to = useRoute().query.to as string | undefined;
|
||||
|
||||
if (session.value !== null) {
|
||||
@@ -95,16 +92,16 @@ const submit = async () => {
|
||||
|
||||
loading.value = true;
|
||||
|
||||
const { data, error } = await authClient.signUp.email({
|
||||
name: form.name,
|
||||
email: form.email,
|
||||
password: form.password,
|
||||
});
|
||||
|
||||
const res = await signUp(form.email, form.password, form.name);
|
||||
loading.value = false;
|
||||
if (!res.ok) {
|
||||
if (!res.error.data) {
|
||||
console.error('Failed to sign up:', res.error);
|
||||
alert('Something went wrong');
|
||||
return;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
const errorCode = error.code! as keyof typeof authClient.$ERROR_CODES;
|
||||
const errorCode = res.error.data.code as keyof typeof authClient.$ERROR_CODES;
|
||||
|
||||
// TODO: i18n
|
||||
// ref https://www.better-auth.com/docs/concepts/client#error-codes
|
||||
@@ -115,33 +112,26 @@ const submit = async () => {
|
||||
emailInputEl.value!.reportValidity();
|
||||
break;
|
||||
case 'INVALID_EMAIL':
|
||||
emailInputEl.value!.setCustomValidity(error.message!);
|
||||
emailInputEl.value!.setCustomValidity(res.error.data.message!);
|
||||
emailInputEl.value!.reportValidity();
|
||||
break;
|
||||
case 'INVALID_PASSWORD':
|
||||
passwordInputEl.value!.setCustomValidity(error.message!);
|
||||
passwordInputEl.value!.setCustomValidity(res.error.data.message!);
|
||||
passwordInputEl.value!.reportValidity();
|
||||
break;
|
||||
default:
|
||||
console.log(error);
|
||||
alert('Something went wrong: ' + error.message);
|
||||
console.log(res.error);
|
||||
alert('Something went wrong: ' + res.error.data.message);
|
||||
break;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const key = await deriveKey(form.password, data.user.id);
|
||||
const key = await deriveKey(form.password, res.data.user.id);
|
||||
localStorage.setItem('encryptionKey', JSON.stringify(key));
|
||||
|
||||
// force a session refetch
|
||||
clearNuxtData();
|
||||
|
||||
// success
|
||||
const triplit = useTriplitClient();
|
||||
if ('startSession' in triplit) {
|
||||
await triplit.startSession(data.token!);
|
||||
}
|
||||
initSettings(res.data.user.id);
|
||||
|
||||
return navigateTo(to ?? '/');
|
||||
};
|
||||
@@ -162,7 +152,7 @@ const submit = async () => {
|
||||
<input required minlength="8" maxlength="128" ref="confirmPasswordInputEl" type="password"
|
||||
autocomplete="new-password" id="confirmPassword" v-model="form.confirmPassword" />
|
||||
<button :disabled="!hydrated" class="accent" type="submit">
|
||||
<Icon v-if="loading" class="text-6" name="svg-spinners:90-ring-with-bg" />
|
||||
<Icon v-if="loading" class="text-6 h-6" name="svg-spinners:90-ring-with-bg" />
|
||||
<span v-else>Register</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
+58
-7
@@ -1,5 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
const { agents } = await useAgents();
|
||||
import type schema from '#triplit/schema';
|
||||
import type { Entity } from '@triplit/client';
|
||||
import { assert } from '~~/utils/assert';
|
||||
|
||||
const { agents, unsubscribe: unsubscribeAgents, createAgent } = await useAgents();
|
||||
const { providers, unsubscribe: unsubscribeModels, getFirstAvailableModel, allModels } = await useModels();
|
||||
|
||||
const taglines = {
|
||||
morning: [
|
||||
@@ -37,6 +42,7 @@ const taglines = {
|
||||
const animatedText = ref('');
|
||||
const currentTaglineIndex = ref(0);
|
||||
const isDeleting = ref(false);
|
||||
let typeWriterInterval: NodeJS.Timeout | null = null;
|
||||
|
||||
const typeWriter = (time: 'morning' | 'afternoon' | 'evening') => {
|
||||
const currentTaglines = taglines[time];
|
||||
@@ -46,7 +52,7 @@ const typeWriter = (time: 'morning' | 'afternoon' | 'evening') => {
|
||||
animatedText.value = currentText.substring(0, animatedText.value.length + 1);
|
||||
if (animatedText.value === currentText) {
|
||||
isDeleting.value = true;
|
||||
setTimeout(() => typeWriter(time), 2000);
|
||||
typeWriterInterval = setTimeout(() => typeWriter(time), 2000);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
@@ -62,13 +68,49 @@ const typeWriter = (time: 'morning' | 'afternoon' | 'evening') => {
|
||||
? baseDeleteSpeed * (0.5 + 0.25 * (animatedText.value.length / currentText.length))
|
||||
: 100;
|
||||
|
||||
setTimeout(() => typeWriter(time), speed);
|
||||
typeWriterInterval = setTimeout(() => typeWriter(time), speed);
|
||||
};
|
||||
|
||||
const handleChatSubmit = async (message: string, _model: unknown) => {
|
||||
const agent = computed(() => {
|
||||
return agents.value?.[0];
|
||||
});
|
||||
|
||||
const handleChatSubmit = async (message: string, model: ModelWithProvider | null) => {
|
||||
console.log('Message submitted:', message, agents);
|
||||
await navigateTo(`/agent/${agents.value![0]!.id}`);
|
||||
// TODO: Implement chat functionality
|
||||
let agent: Entity<typeof schema, 'agents'> | null = agents.value?.[0] ?? null;
|
||||
if (!agent) {
|
||||
const triplit = useTriplitClient();
|
||||
assert('flush' in triplit);
|
||||
|
||||
agent = await createAgent();
|
||||
await triplit.flush();
|
||||
}
|
||||
|
||||
if (!agent) throw new Error('Failed to find agent');
|
||||
|
||||
if (!model) {
|
||||
if (agent.defaultModelId) {
|
||||
model = allModels.value.find(m => m.id === agent.defaultModelId) ?? null;
|
||||
} else {
|
||||
model = getFirstAvailableModel();
|
||||
}
|
||||
}
|
||||
|
||||
if (!model) {
|
||||
console.error('No model selected');
|
||||
return;
|
||||
}
|
||||
|
||||
const { createTopic, autoRename, sendMessage } = useChat(agent.id);
|
||||
|
||||
const topic = await createTopic();
|
||||
if (!topic) throw new Error('Failed to create topic');
|
||||
|
||||
await navigateTo(`/agent/${agent.id}/topic/${topic.id}`);
|
||||
|
||||
autoRename(topic.id, message);
|
||||
|
||||
return sendMessage(message, topic, [], agent, model.provider, model);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
@@ -88,6 +130,14 @@ onMounted(() => {
|
||||
currentTaglineIndex.value = Math.floor(Math.random() * taglines[time].length);
|
||||
typeWriter(time);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (typeWriterInterval !== null) {
|
||||
clearTimeout(typeWriterInterval);
|
||||
}
|
||||
unsubscribeAgents?.();
|
||||
unsubscribeModels?.();
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -95,7 +145,8 @@ onMounted(() => {
|
||||
<h1 class="text-center text-3xl font-semibold">{{ animatedText }}<span class="cursor"> </span></h1>
|
||||
<div class="max-w-4xl h-full w-full">
|
||||
<!-- TODO: view transitions have caused me issues with the page flashing with no content (so just a black or white screen depending on the theme) so I have disabled them for now. -->
|
||||
<ChatInput class="[view-transition-name:chat-prompt] duration-150 ease-in-out" @submit="handleChatSubmit" />
|
||||
<ChatInput class="[view-transition-name:chat-prompt] duration-150 ease-in-out" :agent="agent"
|
||||
:providers="providers" @submit="handleChatSubmit" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -8,10 +8,16 @@ export default defineNuxtPlugin({
|
||||
|
||||
nuxtApp.hook('app:mounted', async () => {
|
||||
if (!session.value) {
|
||||
await fetchSession();
|
||||
const result = await fetchSession();
|
||||
if (!result.ok) {
|
||||
console.warn('Failed to fetch session:', result.error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!session.value) return;
|
||||
if (!session.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ('startSession' in triplit) {
|
||||
await triplit.startSession(session.value.token);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { assert } from "~~/utils/assert";
|
||||
|
||||
export default defineNuxtPlugin({
|
||||
name: 'better-auth-fetch-plugin',
|
||||
enforce: 'pre',
|
||||
@@ -10,16 +12,19 @@ export default defineNuxtPlugin({
|
||||
const { session, fetchSession } = useAuth();
|
||||
|
||||
if (!session.value) {
|
||||
await fetchSession();
|
||||
const result = await fetchSession();
|
||||
if (!result.ok) {
|
||||
console.error('Failed to fetch session:', result.error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!session.value) return;
|
||||
|
||||
if ('updateOptions' in triplit) {
|
||||
triplit.updateOptions({
|
||||
token: session.value.token,
|
||||
});
|
||||
}
|
||||
assert('updateOptions' in triplit);
|
||||
triplit.updateOptions({
|
||||
token: session.value.token,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2,13 +2,17 @@ import { unified } from 'unified';
|
||||
import remarkParse from 'remark-parse';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import remarkRehype from 'remark-rehype';
|
||||
import remarkMath from 'remark-math';
|
||||
import rehypeKatex from 'rehype-katex';
|
||||
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
const remark =
|
||||
unified()
|
||||
.use(remarkParse)
|
||||
.use(remarkGfm)
|
||||
.use(remarkRehype, { allowDangerousHtml: true });
|
||||
.use(remarkMath, { singleDollarTextMath: false })
|
||||
.use(remarkRehype, { allowDangerousHtml: true })
|
||||
.use(rehypeKatex);
|
||||
|
||||
return {
|
||||
provide: {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user