Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f98dea4826 | |||
| 792cc19abe | |||
| 27c9038835 | |||
| 62c45492fa |
29
README.md
29
README.md
@@ -41,20 +41,35 @@ If you want a **small, focused UI for local Ollama usage** → Vessel is built f
|
||||
## Features
|
||||
|
||||
### Chat
|
||||
- Real-time streaming responses
|
||||
- Message editing with branch navigation
|
||||
- Real-time streaming responses with token metrics
|
||||
- **Message branching** — edit any message to create alternative conversation paths
|
||||
- Markdown rendering with syntax highlighting
|
||||
- **Thinking mode** — native support for reasoning models (DeepSeek-R1, etc.)
|
||||
- Dark/Light themes
|
||||
|
||||
### Projects & Organization
|
||||
- **Projects** — group related conversations together
|
||||
- Pin and archive conversations
|
||||
- Smart title generation from conversation content
|
||||
- **Global search** — semantic, title, and content search across all chats
|
||||
|
||||
### Knowledge Base (RAG)
|
||||
- Upload documents (text, markdown, PDF) to build a knowledge base
|
||||
- **Semantic search** using embeddings for context-aware retrieval
|
||||
- Project-specific or global knowledge bases
|
||||
- Automatic context injection into conversations
|
||||
|
||||
### Tools
|
||||
- **5 built-in tools**: web search, URL fetching, calculator, location, time
|
||||
- **Custom tools**: Create your own in JavaScript, Python, or HTTP
|
||||
- Agentic tool calling with chain-of-thought reasoning
|
||||
- Test tools before saving with the built-in testing panel
|
||||
|
||||
### Models
|
||||
- Browse and pull models from ollama.com
|
||||
- Create custom models with embedded system prompts
|
||||
- Track model updates
|
||||
- **Per-model parameters** — customize temperature, context size, top_k/top_p
|
||||
- Track model updates and capability detection (vision, tools, code)
|
||||
|
||||
### Prompts
|
||||
- Save and organize system prompts
|
||||
@@ -145,6 +160,9 @@ Full documentation is available on the **[GitHub Wiki](https://github.com/Viking
|
||||
| Guide | Description |
|
||||
|-------|-------------|
|
||||
| [Getting Started](https://github.com/VikingOwl91/vessel/wiki/Getting-Started) | Installation and configuration |
|
||||
| [Projects](https://github.com/VikingOwl91/vessel/wiki/Projects) | Organize conversations into projects |
|
||||
| [Knowledge Base](https://github.com/VikingOwl91/vessel/wiki/Knowledge-Base) | RAG with document upload and semantic search |
|
||||
| [Search](https://github.com/VikingOwl91/vessel/wiki/Search) | Semantic and content search across chats |
|
||||
| [Custom Tools](https://github.com/VikingOwl91/vessel/wiki/Custom-Tools) | Create JavaScript, Python, or HTTP tools |
|
||||
| [System Prompts](https://github.com/VikingOwl91/vessel/wiki/System-Prompts) | Manage prompts with model defaults |
|
||||
| [Custom Models](https://github.com/VikingOwl91/vessel/wiki/Custom-Models) | Create models with embedded prompts |
|
||||
@@ -164,6 +182,11 @@ Vessel prioritizes **usability and simplicity** over feature breadth.
|
||||
- [x] Custom tools (JavaScript, Python, HTTP)
|
||||
- [x] System prompt library with model-specific defaults
|
||||
- [x] Custom model creation with embedded prompts
|
||||
- [x] Projects for conversation organization
|
||||
- [x] Knowledge base with RAG (semantic retrieval)
|
||||
- [x] Global search (semantic, title, content)
|
||||
- [x] Thinking mode for reasoning models
|
||||
- [x] Message branching and conversation trees
|
||||
|
||||
**Planned:**
|
||||
- [ ] Keyboard-first workflows
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
)
|
||||
|
||||
// Version is set at build time via -ldflags, or defaults to dev
|
||||
var Version = "0.5.1"
|
||||
var Version = "0.5.2"
|
||||
|
||||
func getEnvOrDefault(key, defaultValue string) string {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vessel",
|
||||
"version": "0.5.1",
|
||||
"version": "0.5.2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -98,14 +98,18 @@
|
||||
<!-- Chat icon -->
|
||||
<div class="mt-0.5 shrink-0">
|
||||
{#if conversation.isPinned}
|
||||
<!-- Pin icon for pinned conversations -->
|
||||
<!-- Bookmark icon for pinned conversations -->
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4 text-emerald-500"
|
||||
viewBox="0 0 20 20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M10.75 4.75a.75.75 0 0 0-1.5 0v4.5h-4.5a.75.75 0 0 0 0 1.5h4.5v4.5a.75.75 0 0 0 1.5 0v-4.5h4.5a.75.75 0 0 0 0-1.5h-4.5v-4.5Z" />
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M6.32 2.577a49.255 49.255 0 0 1 11.36 0c1.497.174 2.57 1.46 2.57 2.93V21a.75.75 0 0 1-1.085.67L12 18.089l-7.165 3.583A.75.75 0 0 1 3.75 21V5.507c0-1.47 1.073-2.756 2.57-2.93Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
{:else}
|
||||
<!-- Regular chat bubble -->
|
||||
@@ -147,49 +151,36 @@
|
||||
</div>
|
||||
|
||||
<!-- Action buttons (always visible on mobile, hover on desktop) -->
|
||||
<div class="absolute right-2 top-1/2 flex -translate-y-1/2 items-center gap-1 transition-opacity {uiState.isMobile ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'}">
|
||||
<div class="absolute right-2 top-1/2 flex -translate-y-1/2 items-center gap-0.5 rounded-md bg-theme-secondary/90 px-1 py-0.5 shadow-sm transition-opacity {uiState.isMobile ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'}">
|
||||
<!-- Pin/Unpin button -->
|
||||
<button
|
||||
type="button"
|
||||
onclick={handlePin}
|
||||
class="rounded p-1 text-theme-muted transition-colors hover:bg-theme-tertiary hover:text-theme-primary"
|
||||
class="rounded p-1 transition-colors hover:bg-theme-tertiary {conversation.isPinned ? 'text-emerald-500 hover:text-emerald-400' : 'text-theme-secondary hover:text-theme-primary'}"
|
||||
aria-label={conversation.isPinned ? 'Unpin conversation' : 'Pin conversation'}
|
||||
title={conversation.isPinned ? 'Unpin' : 'Pin'}
|
||||
>
|
||||
{#if conversation.isPinned}
|
||||
<!-- Unpin icon (filled) -->
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M8.75 10.25a.75.75 0 0 0 0 1.5h2.5a.75.75 0 0 0 0-1.5h-2.5Z" />
|
||||
</svg>
|
||||
{:else}
|
||||
<!-- Pin icon (outline) -->
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M3 6.75A.75.75 0 0 1 3.75 6h16.5a.75.75 0 0 1 0 1.5H3.75A.75.75 0 0 1 3 6.75ZM3 12a.75.75 0 0 1 .75-.75h16.5a.75.75 0 0 1 0 1.5H3.75A.75.75 0 0 1 3 12Zm0 5.25a.75.75 0 0 1 .75-.75h16.5a.75.75 0 0 1 0 1.5H3.75a.75.75 0 0 1-.75-.75Z"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4"
|
||||
fill={conversation.isPinned ? 'currentColor' : 'none'}
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M17.593 3.322c1.1.128 1.907 1.077 1.907 2.185V21L12 17.25 4.5 21V5.507c0-1.108.806-2.057 1.907-2.185a48.507 48.507 0 0 1 11.186 0Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Move to project button -->
|
||||
<button
|
||||
type="button"
|
||||
onclick={handleMove}
|
||||
class="rounded p-1 text-theme-muted transition-colors hover:bg-theme-tertiary hover:text-theme-primary"
|
||||
class="rounded p-1 text-theme-secondary transition-colors hover:bg-theme-tertiary hover:text-theme-primary"
|
||||
aria-label="Move to project"
|
||||
title="Move to project"
|
||||
>
|
||||
@@ -213,7 +204,7 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={handleExport}
|
||||
class="rounded p-1 text-theme-muted transition-colors hover:bg-theme-tertiary hover:text-theme-primary"
|
||||
class="rounded p-1 text-theme-secondary transition-colors hover:bg-theme-tertiary hover:text-theme-primary"
|
||||
aria-label="Export conversation"
|
||||
title="Export"
|
||||
>
|
||||
@@ -237,7 +228,7 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={handleDelete}
|
||||
class="rounded p-1 text-theme-muted transition-colors hover:bg-red-900/50 hover:text-red-400"
|
||||
class="rounded p-1 text-theme-secondary transition-colors hover:bg-red-900/50 hover:text-red-400"
|
||||
aria-label="Delete conversation"
|
||||
title="Delete"
|
||||
>
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={handleSettings}
|
||||
class="shrink-0 rounded p-0.5 text-theme-muted opacity-0 transition-opacity hover:bg-theme-tertiary hover:text-theme-primary group-hover:opacity-100"
|
||||
class="shrink-0 rounded p-1 text-theme-secondary opacity-0 transition-opacity hover:bg-theme-tertiary hover:text-theme-primary group-hover:opacity-100"
|
||||
aria-label="Project settings"
|
||||
title="Settings"
|
||||
>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
import type { Conversation } from '$lib/types/conversation.js';
|
||||
import { pinConversation, archiveConversation } from '$lib/storage/conversations.js';
|
||||
|
||||
/** Date group labels */
|
||||
type DateGroup = 'Today' | 'Yesterday' | 'Previous 7 Days' | 'Previous 30 Days' | 'Older';
|
||||
@@ -161,23 +162,43 @@ export class ConversationsState {
|
||||
|
||||
/**
|
||||
* Toggle pin status of a conversation
|
||||
* Persists to IndexedDB and queues for backend sync
|
||||
* @param id The conversation ID
|
||||
*/
|
||||
pin(id: string): void {
|
||||
async pin(id: string): Promise<void> {
|
||||
const conversation = this.items.find((c) => c.id === id);
|
||||
if (conversation) {
|
||||
// Update in-memory state immediately for responsive UI
|
||||
this.update(id, { isPinned: !conversation.isPinned });
|
||||
|
||||
// Persist to IndexedDB and queue for sync
|
||||
const result = await pinConversation(id);
|
||||
if (!result.success) {
|
||||
// Revert on failure
|
||||
this.update(id, { isPinned: conversation.isPinned });
|
||||
console.error('Failed to persist pin state:', result.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle archive status of a conversation
|
||||
* Persists to IndexedDB and queues for backend sync
|
||||
* @param id The conversation ID
|
||||
*/
|
||||
archive(id: string): void {
|
||||
async archive(id: string): Promise<void> {
|
||||
const conversation = this.items.find((c) => c.id === id);
|
||||
if (conversation) {
|
||||
// Update in-memory state immediately for responsive UI
|
||||
this.update(id, { isArchived: !conversation.isArchived });
|
||||
|
||||
// Persist to IndexedDB and queue for sync
|
||||
const result = await archiveConversation(id);
|
||||
if (!result.success) {
|
||||
// Revert on failure
|
||||
this.update(id, { isArchived: conversation.isArchived });
|
||||
console.error('Failed to persist archive state:', result.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user