mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-07-30 19:02:16 +00:00
fixing ui + adding diff fonts + fixing merge conflicts
This commit is contained in:
+56
-74
@@ -5,9 +5,7 @@ hide:
|
||||
- navigation
|
||||
---
|
||||
|
||||
# OpenJarvis
|
||||
|
||||
**Programming abstractions for on-device AI.**
|
||||
# _Programming abstractions_ for on-device AI
|
||||
|
||||
OpenJarvis is a modular framework for building, running, and learning from local AI systems. It provides composable abstractions across **five pillars** with a cross-cutting trace-driven learning system:
|
||||
|
||||
@@ -29,55 +27,55 @@ Everything runs on your hardware. Cloud APIs are optional.
|
||||
|
||||
---
|
||||
|
||||
Intelligence (the model), Agents (agentic harness), Tools (MCP-based tool system with storage), Engine (inference runtime), and Learning (trace-driven improvement) — each with a clear ABC interface and decorator-based registry.
|
||||
Intelligence, Agents, Tools, Engine, and Learning — each with a clear ABC interface and decorator-based registry.
|
||||
|
||||
- **5 Engine Backends**
|
||||
|
||||
---
|
||||
|
||||
Ollama, vLLM, SGLang, llama.cpp, and cloud (OpenAI/Anthropic/Google). All implement the same `InferenceEngine` ABC with `generate()`, `stream()`, `list_models()`, and `health()`.
|
||||
Ollama, vLLM, SGLang, llama.cpp, and cloud (OpenAI/Anthropic/Google). Same `InferenceEngine` ABC.
|
||||
|
||||
- **5 Memory Backends**
|
||||
|
||||
---
|
||||
|
||||
SQLite/FTS5 (default, zero-dependency), FAISS, ColBERTv2, BM25, and Hybrid (reciprocal rank fusion). Document chunking, indexing, and context injection built in.
|
||||
SQLite/FTS5, FAISS, ColBERTv2, BM25, and Hybrid (reciprocal rank fusion). Zero-dependency default.
|
||||
|
||||
- **Hardware-Aware**
|
||||
|
||||
---
|
||||
|
||||
Auto-detects GPU vendor, model, and VRAM via `nvidia-smi`, `rocm-smi`, and `system_profiler`. Recommends the optimal engine for your hardware automatically.
|
||||
Auto-detects GPU vendor, model, and VRAM. Recommends the optimal engine for your hardware.
|
||||
|
||||
- **Offline-First**
|
||||
|
||||
---
|
||||
|
||||
All core functionality works without a network connection. Cloud API backends are optional extras for when you need them.
|
||||
All core functionality works without a network connection. Cloud APIs are optional extras.
|
||||
|
||||
- **OpenAI-Compatible API**
|
||||
|
||||
---
|
||||
|
||||
`jarvis serve` starts a FastAPI server with `POST /v1/chat/completions`, `GET /v1/models`, and SSE streaming. Drop-in replacement for OpenAI-compatible clients.
|
||||
`jarvis serve` starts a FastAPI server with SSE streaming. Drop-in replacement for OpenAI clients.
|
||||
|
||||
- **Trace-Driven Learning**
|
||||
|
||||
---
|
||||
|
||||
Every agent interaction is recorded as a trace. The learning system improves Intelligence (SFT weight updates) and Agents (system prompt, tool selection, retry logic). Pluggable policies: heuristic, trace-driven, SFT, agent advisor, ICL updater, GRPO.
|
||||
Every interaction is traced. The learning system improves models (SFT) and agents (prompt, tools, logic).
|
||||
|
||||
- **Python SDK**
|
||||
|
||||
---
|
||||
|
||||
The `Jarvis` class provides a high-level sync API. Three lines of code to ask a question. Full access to agents, tools, memory, and model routing.
|
||||
The `Jarvis` class: three lines of code to ask a question. Full access to agents, tools, memory, and routing.
|
||||
|
||||
- **CLI-First**
|
||||
|
||||
---
|
||||
|
||||
`jarvis ask`, `jarvis serve`, `jarvis memory`, `jarvis bench`, `jarvis telemetry` — every capability is accessible from the command line with rich terminal output.
|
||||
`jarvis ask`, `jarvis serve`, `jarvis memory`, `jarvis bench` — every capability from the command line.
|
||||
|
||||
</div>
|
||||
|
||||
@@ -85,86 +83,70 @@ Everything runs on your hardware. Cloud APIs are optional.
|
||||
|
||||
## Quick Start
|
||||
|
||||
<div class="quick-start-section" markdown>
|
||||
=== "Browser App"
|
||||
|
||||
### Browser App
|
||||
Run the full chat UI locally with one script:
|
||||
|
||||
Run the full chat UI locally with one script:
|
||||
```bash
|
||||
git clone https://github.com/HazyResearch/OpenJarvis.git
|
||||
cd OpenJarvis
|
||||
./scripts/quickstart.sh
|
||||
```
|
||||
|
||||
```bash
|
||||
git clone https://github.com/HazyResearch/OpenJarvis.git
|
||||
cd OpenJarvis
|
||||
./scripts/quickstart.sh
|
||||
```
|
||||
This installs dependencies, starts Ollama + a local model, launches the backend
|
||||
and frontend, and opens `http://localhost:5173` in your browser.
|
||||
|
||||
This installs dependencies, starts Ollama + a local model, launches the backend
|
||||
and frontend, and opens `http://localhost:5173` in your browser.
|
||||
=== "Desktop App"
|
||||
|
||||
</div>
|
||||
Download the native desktop app — it bundles Ollama and the Python backend
|
||||
so everything works out of the box.
|
||||
|
||||
<div class="quick-start-section" markdown>
|
||||
[Download for macOS (Apple Silicon)](https://github.com/HazyResearch/OpenJarvis/releases/latest/download/OpenJarvis_aarch64.dmg){ .md-button .md-button--primary }
|
||||
|
||||
### Desktop App
|
||||
Also available for [macOS (Intel)](https://github.com/HazyResearch/OpenJarvis/releases/latest/download/OpenJarvis_x64.dmg), [Windows](https://github.com/HazyResearch/OpenJarvis/releases/latest/download/OpenJarvis_x64-setup.exe), [Linux (DEB)](https://github.com/HazyResearch/OpenJarvis/releases/latest/download/OpenJarvis_amd64.deb), and [Linux (RPM)](https://github.com/HazyResearch/OpenJarvis/releases/latest/download/OpenJarvis_amd64.rpm). See the [Downloads](downloads.md) page for details.
|
||||
|
||||
Download the native desktop app — it bundles Ollama and the Python backend
|
||||
so everything works out of the box.
|
||||
=== "Python SDK"
|
||||
|
||||
[Download for macOS (Apple Silicon)](https://github.com/HazyResearch/OpenJarvis/releases/latest/download/OpenJarvis_aarch64.dmg){ .md-button .md-button--primary }
|
||||
```python
|
||||
from openjarvis import Jarvis
|
||||
|
||||
Also available for [macOS (Intel)](https://github.com/HazyResearch/OpenJarvis/releases/latest/download/OpenJarvis_x64.dmg), [Windows](https://github.com/HazyResearch/OpenJarvis/releases/latest/download/OpenJarvis_x64-setup.exe), [Linux (DEB)](https://github.com/HazyResearch/OpenJarvis/releases/latest/download/OpenJarvis_amd64.deb), and [Linux (RPM)](https://github.com/HazyResearch/OpenJarvis/releases/latest/download/OpenJarvis_amd64.rpm). See the [Downloads](downloads.md) page for details.
|
||||
j = Jarvis()
|
||||
response = j.ask("Explain quicksort in two sentences.")
|
||||
print(response)
|
||||
j.close()
|
||||
```
|
||||
|
||||
</div>
|
||||
For more control, use `ask_full()` to get usage stats, model info, and tool results:
|
||||
|
||||
<div class="quick-start-section" markdown>
|
||||
```python
|
||||
result = j.ask_full(
|
||||
"What is 2 + 2?",
|
||||
agent="orchestrator",
|
||||
tools=["calculator"],
|
||||
)
|
||||
print(result["content"]) # "4"
|
||||
print(result["tool_results"]) # [{tool_name: "calculator", ...}]
|
||||
```
|
||||
|
||||
### Python SDK
|
||||
=== "CLI"
|
||||
|
||||
```python
|
||||
from openjarvis import Jarvis
|
||||
```bash
|
||||
# Ask a question
|
||||
jarvis ask "What is the capital of France?"
|
||||
|
||||
j = Jarvis()
|
||||
response = j.ask("Explain quicksort in two sentences.")
|
||||
print(response)
|
||||
j.close()
|
||||
```
|
||||
# Use an agent with tools
|
||||
jarvis ask --agent orchestrator --tools calculator,think "What is 137 * 42?"
|
||||
|
||||
For more control, use `ask_full()` to get usage stats, model info, and tool results:
|
||||
# Start the API server
|
||||
jarvis serve --port 8000
|
||||
|
||||
```python
|
||||
result = j.ask_full(
|
||||
"What is 2 + 2?",
|
||||
agent="orchestrator",
|
||||
tools=["calculator"],
|
||||
)
|
||||
print(result["content"]) # "4"
|
||||
print(result["tool_results"]) # [{tool_name: "calculator", ...}]
|
||||
```
|
||||
# Index documents and search memory
|
||||
jarvis memory index ./docs/
|
||||
jarvis memory search "configuration options"
|
||||
|
||||
</div>
|
||||
|
||||
<div class="quick-start-section" markdown>
|
||||
|
||||
### CLI
|
||||
|
||||
```bash
|
||||
# Ask a question
|
||||
jarvis ask "What is the capital of France?"
|
||||
|
||||
# Use an agent with tools
|
||||
jarvis ask --agent orchestrator --tools calculator,think "What is 137 * 42?"
|
||||
|
||||
# Start the API server
|
||||
jarvis serve --port 8000
|
||||
|
||||
# Index documents and search memory
|
||||
jarvis memory index ./docs/
|
||||
jarvis memory search "configuration options"
|
||||
|
||||
# Run inference benchmarks
|
||||
jarvis bench run --json
|
||||
```
|
||||
|
||||
</div>
|
||||
# Run inference benchmarks
|
||||
jarvis bench run --json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
||||
+171
-236
@@ -1,200 +1,89 @@
|
||||
/* ── Fonts ─────────────────────────────────────────────────────────── */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Merriweather:ital,wght@0,300;0,400;0,700;0,900;1,300;1,400;1,700&family=Inter:wght@400;500;600;700&display=swap');
|
||||
|
||||
:root {
|
||||
--md-text-font: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
--md-text-font: "Merriweather", Georgia, "Times New Roman", serif;
|
||||
--md-code-font: "JetBrains Mono", "Fira Code", "SF Mono", monospace;
|
||||
}
|
||||
|
||||
/* ── Color Overrides (Light) ──────────────────────────────────────── */
|
||||
[data-md-color-scheme="default"] {
|
||||
--md-primary-fg-color: #1e293b;
|
||||
--md-primary-fg-color--light: #334155;
|
||||
--md-primary-fg-color--dark: #0f172a;
|
||||
--md-accent-fg-color: #3b82f6;
|
||||
--md-accent-fg-color--transparent: rgba(59, 130, 246, 0.1);
|
||||
--md-primary-fg-color: #1a1a2e;
|
||||
--md-primary-fg-color--light: #2d2d44;
|
||||
--md-primary-fg-color--dark: #0d0d1a;
|
||||
--md-accent-fg-color: #4361ee;
|
||||
--md-accent-fg-color--transparent: rgba(67, 97, 238, 0.08);
|
||||
--md-default-bg-color: #ffffff;
|
||||
--md-default-fg-color: #1e293b;
|
||||
--md-default-fg-color--light: #475569;
|
||||
--md-typeset-a-color: #2563eb;
|
||||
--md-default-fg-color: #1a1a2e;
|
||||
--md-default-fg-color--light: #555577;
|
||||
--md-typeset-a-color: #4361ee;
|
||||
}
|
||||
|
||||
/* ── Color Overrides (Dark) ───────────────────────────────────────── */
|
||||
[data-md-color-scheme="slate"] {
|
||||
--md-primary-fg-color: #0f172a;
|
||||
--md-primary-fg-color--light: #1e293b;
|
||||
--md-primary-fg-color--dark: #020617;
|
||||
--md-accent-fg-color: #60a5fa;
|
||||
--md-accent-fg-color--transparent: rgba(96, 165, 250, 0.1);
|
||||
--md-default-bg-color: #0f172a;
|
||||
--md-default-fg-color: #e2e8f0;
|
||||
--md-default-fg-color--light: #94a3b8;
|
||||
--md-typeset-a-color: #60a5fa;
|
||||
--md-code-bg-color: #1e293b;
|
||||
--md-primary-fg-color: #0d0d1a;
|
||||
--md-primary-fg-color--light: #1a1a2e;
|
||||
--md-primary-fg-color--dark: #050510;
|
||||
--md-accent-fg-color: #7b8cde;
|
||||
--md-accent-fg-color--transparent: rgba(123, 140, 222, 0.1);
|
||||
--md-default-bg-color: #0d0d1a;
|
||||
--md-default-fg-color: #e0e0ec;
|
||||
--md-default-fg-color--light: #8888aa;
|
||||
--md-typeset-a-color: #7b8cde;
|
||||
--md-code-bg-color: #161628;
|
||||
}
|
||||
|
||||
/* ── Hero Section ─────────────────────────────────────────────────── */
|
||||
.hero {
|
||||
padding: 4rem 0 3rem;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
/* ── Global Typography ───────────────────────────────────────────── */
|
||||
.md-typeset {
|
||||
font-size: 0.92rem;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
font-size: 3.2rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.03em;
|
||||
line-height: 1.1;
|
||||
margin-bottom: 1rem;
|
||||
background: linear-gradient(135deg, #1e293b 0%, #3b82f6 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
/* Nav and UI chrome use a clean sans-serif */
|
||||
.md-header,
|
||||
.md-tabs,
|
||||
.md-nav,
|
||||
.md-footer,
|
||||
.md-search__input,
|
||||
.md-source {
|
||||
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .hero h1 {
|
||||
background: linear-gradient(135deg, #e2e8f0 0%, #60a5fa 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
.md-nav__link,
|
||||
.md-tabs__link {
|
||||
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.hero .hero-tagline {
|
||||
font-size: 1.25rem;
|
||||
/* ── Hero Section (DSPy-style: clean, italic emphasis) ───────────── */
|
||||
.md-typeset h1 {
|
||||
font-size: 2.6rem;
|
||||
font-weight: 900;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.15;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--md-default-fg-color);
|
||||
}
|
||||
|
||||
.md-typeset h1 em {
|
||||
font-style: italic;
|
||||
color: var(--md-accent-fg-color);
|
||||
}
|
||||
|
||||
.hero-tagline {
|
||||
font-size: 1.15rem;
|
||||
color: var(--md-default-fg-color--light);
|
||||
max-width: 640px;
|
||||
margin: 0 auto 2rem;
|
||||
line-height: 1.6;
|
||||
line-height: 1.7;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.hero-buttons {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.hero-buttons a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1.75rem;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.hero-buttons .btn-primary {
|
||||
background: #3b82f6;
|
||||
color: #fff;
|
||||
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
.hero-buttons .btn-primary:hover {
|
||||
background: #2563eb;
|
||||
box-shadow: 0 4px 16px rgba(59, 130, 246, 0.4);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.hero-buttons .btn-secondary {
|
||||
background: transparent;
|
||||
color: var(--md-default-fg-color);
|
||||
border: 1.5px solid var(--md-default-fg-color--light);
|
||||
}
|
||||
|
||||
.hero-buttons .btn-secondary:hover {
|
||||
border-color: var(--md-accent-fg-color);
|
||||
color: var(--md-accent-fg-color);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* ── Feature Cards ────────────────────────────────────────────────── */
|
||||
.md-typeset .grid.cards > ol,
|
||||
.md-typeset .grid.cards > ul {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.md-typeset .grid.cards > ol > li,
|
||||
.md-typeset .grid.cards > ul > li {
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
transition: all 0.25s ease;
|
||||
background: var(--md-default-bg-color);
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .md-typeset .grid.cards > ol > li,
|
||||
[data-md-color-scheme="slate"] .md-typeset .grid.cards > ul > li {
|
||||
border-color: rgba(255, 255, 255, 0.08);
|
||||
background: #1e293b;
|
||||
}
|
||||
|
||||
.md-typeset .grid.cards > ol > li:hover,
|
||||
.md-typeset .grid.cards > ul > li:hover {
|
||||
border-color: var(--md-accent-fg-color);
|
||||
box-shadow: 0 4px 20px rgba(59, 130, 246, 0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.md-typeset .grid.cards > ol > li > :first-child,
|
||||
.md-typeset .grid.cards > ul > li > :first-child {
|
||||
font-weight: 600;
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
/* ── Code Blocks ──────────────────────────────────────────────────── */
|
||||
.md-typeset code {
|
||||
border-radius: 4px;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.md-typeset pre {
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .md-typeset pre {
|
||||
border-color: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.md-typeset pre > code {
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
/* ── Tabs ─────────────────────────────────────────────────────────── */
|
||||
.md-typeset .tabbed-labels > label {
|
||||
font-weight: 500;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* ── Navigation ───────────────────────────────────────────────────── */
|
||||
.md-tabs {
|
||||
background: var(--md-primary-fg-color);
|
||||
}
|
||||
|
||||
.md-header {
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
/* ── Admonitions ──────────────────────────────────────────────────── */
|
||||
.md-typeset .admonition,
|
||||
.md-typeset details {
|
||||
border-radius: 8px;
|
||||
border-width: 1px;
|
||||
border-left-width: 4px;
|
||||
box-shadow: none;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
/* ── Section Headers ──────────────────────────────────────────────── */
|
||||
.md-typeset h2 {
|
||||
font-weight: 700;
|
||||
font-size: 1.6rem;
|
||||
letter-spacing: -0.02em;
|
||||
font-size: 1.5rem;
|
||||
letter-spacing: -0.015em;
|
||||
margin-top: 3rem;
|
||||
padding-top: 1.5rem;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.06);
|
||||
@@ -211,8 +100,93 @@
|
||||
}
|
||||
|
||||
.md-typeset h3 {
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.md-typeset h4 {
|
||||
font-weight: 600;
|
||||
font-size: 1.15rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
/* ── Feature Cards (simplified, DSPy-like) ───────────────────────── */
|
||||
.md-typeset .grid.cards > ol,
|
||||
.md-typeset .grid.cards > ul {
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.md-typeset .grid.cards > ol > li,
|
||||
.md-typeset .grid.cards > ul > li {
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
border-radius: 8px;
|
||||
padding: 1.25rem 1.5rem;
|
||||
transition: border-color 0.2s ease;
|
||||
background: var(--md-default-bg-color);
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .md-typeset .grid.cards > ol > li,
|
||||
[data-md-color-scheme="slate"] .md-typeset .grid.cards > ul > li {
|
||||
border-color: rgba(255, 255, 255, 0.08);
|
||||
background: #161628;
|
||||
}
|
||||
|
||||
.md-typeset .grid.cards > ol > li:hover,
|
||||
.md-typeset .grid.cards > ul > li:hover {
|
||||
border-color: var(--md-accent-fg-color);
|
||||
}
|
||||
|
||||
.md-typeset .grid.cards > ol > li > :first-child,
|
||||
.md-typeset .grid.cards > ul > li > :first-child {
|
||||
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
/* ── Code Blocks ──────────────────────────────────────────────────── */
|
||||
.md-typeset code {
|
||||
border-radius: 4px;
|
||||
font-size: 0.82em;
|
||||
}
|
||||
|
||||
.md-typeset pre {
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .md-typeset pre {
|
||||
border-color: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.md-typeset pre > code {
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
/* ── Tabs ─────────────────────────────────────────────────────────── */
|
||||
.md-typeset .tabbed-labels > label {
|
||||
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
font-weight: 500;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
/* ── Navigation ───────────────────────────────────────────────────── */
|
||||
.md-tabs {
|
||||
background: var(--md-primary-fg-color);
|
||||
}
|
||||
|
||||
.md-header {
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
/* ── Admonitions ──────────────────────────────────────────────────── */
|
||||
.md-typeset .admonition,
|
||||
.md-typeset details {
|
||||
border-radius: 8px;
|
||||
border-width: 1px;
|
||||
border-left-width: 4px;
|
||||
box-shadow: none;
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
/* ── Footer ───────────────────────────────────────────────────────── */
|
||||
@@ -220,14 +194,15 @@
|
||||
background: var(--md-primary-fg-color--dark);
|
||||
}
|
||||
|
||||
/* ── Pill Badges (used on landing page) ───────────────────────────── */
|
||||
/* ── Pill Badges ─────────────────────────────────────────────────── */
|
||||
.pill {
|
||||
display: inline-block;
|
||||
padding: 0.3rem 0.9rem;
|
||||
padding: 0.25rem 0.8rem;
|
||||
border-radius: 100px;
|
||||
font-size: 0.8rem;
|
||||
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
letter-spacing: 0.03em;
|
||||
background: var(--md-accent-fg-color--transparent);
|
||||
color: var(--md-accent-fg-color);
|
||||
margin-bottom: 1rem;
|
||||
@@ -236,27 +211,13 @@
|
||||
/* ── Divider ──────────────────────────────────────────────────────── */
|
||||
.md-typeset hr {
|
||||
border-color: rgba(0, 0, 0, 0.06);
|
||||
margin: 3rem 0;
|
||||
margin: 2.5rem 0;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .md-typeset hr {
|
||||
border-color: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
/* ── Responsive ───────────────────────────────────────────────────── */
|
||||
@media (max-width: 768px) {
|
||||
.hero h1 {
|
||||
font-size: 2.2rem;
|
||||
}
|
||||
.hero .hero-tagline {
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
.hero-buttons {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Inline Code ─────────────────────────────────────────────────── */
|
||||
.md-typeset :not(pre) > code {
|
||||
transition: background 0.15s ease;
|
||||
@@ -271,17 +232,21 @@
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
|
||||
.md-typeset a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* ── Content Width ───────────────────────────────────────────────── */
|
||||
.md-content__inner {
|
||||
max-width: 52rem;
|
||||
max-width: 50rem;
|
||||
}
|
||||
|
||||
/* ── Tables ──────────────────────────────────────────────────────── */
|
||||
.md-typeset table:not([class]) {
|
||||
border-radius: 8px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
font-size: 0.85rem;
|
||||
font-size: 0.83rem;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .md-typeset table:not([class]) {
|
||||
@@ -289,58 +254,28 @@
|
||||
}
|
||||
|
||||
.md-typeset table:not([class]) th {
|
||||
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 0.8rem;
|
||||
font-size: 0.78rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
/* ── Quick-start section cards ────────────────────────────────────── */
|
||||
.quick-start-section {
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
border-left: 4px solid var(--md-accent-fg-color);
|
||||
border-radius: 10px;
|
||||
padding: 1.5rem 2rem 1.25rem;
|
||||
margin-bottom: 1.5rem;
|
||||
background: var(--md-default-bg-color);
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
/* ── Quick Start Tabs (replaces old card sections) ───────────────── */
|
||||
.md-typeset .tabbed-set {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.quick-start-section:hover {
|
||||
border-color: rgba(0, 0, 0, 0.12);
|
||||
border-left-color: var(--md-accent-fg-color);
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
|
||||
.md-typeset .tabbed-content {
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .quick-start-section {
|
||||
border-color: rgba(255, 255, 255, 0.08);
|
||||
border-left-color: var(--md-accent-fg-color);
|
||||
background: #1e293b;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .quick-start-section:hover {
|
||||
border-color: rgba(255, 255, 255, 0.14);
|
||||
border-left-color: var(--md-accent-fg-color);
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.quick-start-section h3 {
|
||||
font-size: 1.3rem !important;
|
||||
font-weight: 700 !important;
|
||||
margin-top: 0 !important;
|
||||
margin-bottom: 0.75rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .quick-start-section h3 {
|
||||
border-bottom-color: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.quick-start-section p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.quick-start-section pre:last-child {
|
||||
margin-bottom: 0;
|
||||
/* ── Responsive ───────────────────────────────────────────────────── */
|
||||
@media (max-width: 768px) {
|
||||
.md-typeset h1 {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
.hero-tagline {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
<meta name="description" content="OpenJarvis — on-device AI assistant" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Merriweather:ital,wght@0,300;0,400;0,700;0,900;1,300;1,400;1,700&display=swap" rel="stylesheet" />
|
||||
<title>OpenJarvis</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -17,6 +17,7 @@ export function InputArea() {
|
||||
const selectedModel = useAppStore((s) => s.selectedModel);
|
||||
const streamState = useAppStore((s) => s.streamState);
|
||||
const messages = useAppStore((s) => s.messages);
|
||||
const speechEnabled = useAppStore((s) => s.settings.speechEnabled);
|
||||
const createConversation = useAppStore((s) => s.createConversation);
|
||||
const addMessage = useAppStore((s) => s.addMessage);
|
||||
const updateLastAssistant = useAppStore((s) => s.updateLastAssistant);
|
||||
@@ -25,6 +26,13 @@ export function InputArea() {
|
||||
|
||||
const { state: speechState, available: speechAvailable, startRecording, stopRecording } = useSpeech();
|
||||
|
||||
const micDisabled = !speechEnabled || !speechAvailable || streamState.isStreaming;
|
||||
const micReason: 'not-enabled' | 'no-backend' | 'streaming' | undefined =
|
||||
!speechEnabled ? 'not-enabled'
|
||||
: !speechAvailable ? 'no-backend'
|
||||
: streamState.isStreaming ? 'streaming'
|
||||
: undefined;
|
||||
|
||||
const handleMicClick = useCallback(async () => {
|
||||
if (speechState === 'recording') {
|
||||
try {
|
||||
@@ -257,13 +265,12 @@ export function InputArea() {
|
||||
</button>
|
||||
) : (
|
||||
<div className="flex items-center gap-1">
|
||||
{speechAvailable && (
|
||||
<MicButton
|
||||
state={speechState}
|
||||
onClick={handleMicClick}
|
||||
disabled={streamState.isStreaming}
|
||||
/>
|
||||
)}
|
||||
<MicButton
|
||||
state={speechState}
|
||||
onClick={handleMicClick}
|
||||
disabled={micDisabled}
|
||||
reason={micReason}
|
||||
/>
|
||||
<button
|
||||
onClick={sendMessage}
|
||||
disabled={!input.trim()}
|
||||
|
||||
@@ -1,53 +1,80 @@
|
||||
import { useState } from 'react';
|
||||
import type { SpeechState } from '../../hooks/useSpeech';
|
||||
|
||||
interface MicButtonProps {
|
||||
state: SpeechState;
|
||||
onClick: () => void;
|
||||
disabled?: boolean;
|
||||
reason?: 'not-enabled' | 'no-backend' | 'streaming';
|
||||
}
|
||||
|
||||
export function MicButton({ state, onClick, disabled }: MicButtonProps) {
|
||||
const title =
|
||||
state === 'recording'
|
||||
? 'Stop recording'
|
||||
: state === 'transcribing'
|
||||
? 'Transcribing...'
|
||||
: 'Voice input';
|
||||
export function MicButton({ state, onClick, disabled, reason }: MicButtonProps) {
|
||||
const [showTooltip, setShowTooltip] = useState(false);
|
||||
|
||||
const tooltipText =
|
||||
reason === 'not-enabled'
|
||||
? 'Enable in Settings'
|
||||
: reason === 'no-backend'
|
||||
? 'Speech backend not configured'
|
||||
: reason === 'streaming'
|
||||
? 'Wait for response'
|
||||
: state === 'recording'
|
||||
? 'Stop recording'
|
||||
: state === 'transcribing'
|
||||
? 'Transcribing...'
|
||||
: 'Voice input';
|
||||
|
||||
const isInactive = disabled || state === 'transcribing';
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`mic-btn ${state !== 'idle' ? `mic-${state}` : ''}`}
|
||||
onClick={onClick}
|
||||
disabled={disabled || state === 'transcribing'}
|
||||
title={title}
|
||||
style={{
|
||||
background: state === 'recording' ? '#e74c3c' : 'transparent',
|
||||
border: '1px solid var(--border, #555)',
|
||||
borderRadius: '8px',
|
||||
padding: '8px',
|
||||
cursor: disabled || state === 'transcribing' ? 'default' : 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
minWidth: '36px',
|
||||
height: '36px',
|
||||
color: state === 'recording' ? '#fff' : 'var(--text, #cdd6f4)',
|
||||
opacity: disabled || state === 'transcribing' ? 0.5 : 1,
|
||||
animation: state === 'recording' ? 'pulse 1.5s ease-in-out infinite' : 'none',
|
||||
}}
|
||||
<div
|
||||
className="relative"
|
||||
onMouseEnter={() => setShowTooltip(true)}
|
||||
onMouseLeave={() => setShowTooltip(false)}
|
||||
>
|
||||
{state === 'transcribing' ? (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<circle cx="8" cy="8" r="6" fill="none" stroke="currentColor" strokeWidth="2" strokeDasharray="28" strokeDashoffset="10">
|
||||
<animateTransform attributeName="transform" type="rotate" from="0 8 8" to="360 8 8" dur="1s" repeatCount="indefinite" />
|
||||
</circle>
|
||||
</svg>
|
||||
) : (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M5 3a3 3 0 0 1 6 0v5a3 3 0 0 1-6 0V3z" />
|
||||
<path d="M3.5 6.5A.5.5 0 0 1 4 7v1a4 4 0 0 0 8 0V7a.5.5 0 0 1 1 0v1a5 5 0 0 1-4.5 4.975V15h3a.5.5 0 0 1 0 1h-7a.5.5 0 0 1 0-1h3v-2.025A5 5 0 0 1 3 8V7a.5.5 0 0 1 .5-.5z" />
|
||||
</svg>
|
||||
<button
|
||||
onClick={onClick}
|
||||
disabled={isInactive}
|
||||
className="p-2 rounded-xl transition-all shrink-0"
|
||||
style={{
|
||||
background: state === 'recording'
|
||||
? 'var(--color-error)'
|
||||
: 'transparent',
|
||||
color: state === 'recording'
|
||||
? 'white'
|
||||
: isInactive
|
||||
? 'var(--color-text-tertiary)'
|
||||
: 'var(--color-text-secondary)',
|
||||
cursor: isInactive ? 'default' : 'pointer',
|
||||
opacity: isInactive ? 0.35 : 1,
|
||||
animation: state === 'recording' ? 'pulse 1.5s ease-in-out infinite' : 'none',
|
||||
}}
|
||||
>
|
||||
{state === 'transcribing' ? (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<circle cx="8" cy="8" r="6" fill="none" stroke="currentColor" strokeWidth="2" strokeDasharray="28" strokeDashoffset="10">
|
||||
<animateTransform attributeName="transform" type="rotate" from="0 8 8" to="360 8 8" dur="1s" repeatCount="indefinite" />
|
||||
</circle>
|
||||
</svg>
|
||||
) : (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M5 3a3 3 0 0 1 6 0v5a3 3 0 0 1-6 0V3z" />
|
||||
<path d="M3.5 6.5A.5.5 0 0 1 4 7v1a4 4 0 0 0 8 0V7a.5.5 0 0 1 1 0v1a5 5 0 0 1-4.5 4.975V15h3a.5.5 0 0 1 0 1h-7a.5.5 0 0 1 0-1h3v-2.025A5 5 0 0 1 3 8V7a.5.5 0 0 1 .5-.5z" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
{showTooltip && isInactive && (
|
||||
<div
|
||||
className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2.5 py-1.5 rounded-lg text-xs whitespace-nowrap pointer-events-none"
|
||||
style={{
|
||||
background: 'var(--color-text)',
|
||||
color: 'var(--color-bg)',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
|
||||
}}
|
||||
>
|
||||
{tooltipText}
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -138,7 +138,9 @@
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
font-family: "Merriweather", Georgia, "Times New Roman", serif;
|
||||
font-weight: 400;
|
||||
line-height: 1.7;
|
||||
background-color: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
@@ -234,7 +236,7 @@
|
||||
|
||||
/* Markdown prose styling */
|
||||
.prose {
|
||||
line-height: 1.65;
|
||||
line-height: 1.75;
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ interface Settings {
|
||||
defaultAgent: string;
|
||||
temperature: number;
|
||||
maxTokens: number;
|
||||
speechEnabled: boolean;
|
||||
}
|
||||
|
||||
function loadSettings(): Settings {
|
||||
@@ -62,6 +63,7 @@ function loadSettings(): Settings {
|
||||
defaultAgent: '',
|
||||
temperature: 0.7,
|
||||
maxTokens: 4096,
|
||||
speechEnabled: false,
|
||||
};
|
||||
try {
|
||||
const raw = localStorage.getItem(SETTINGS_KEY);
|
||||
|
||||
@@ -13,9 +13,10 @@ import {
|
||||
Download,
|
||||
Upload,
|
||||
Trash2,
|
||||
Mic,
|
||||
} from 'lucide-react';
|
||||
import { useAppStore, type ThemeMode } from '../lib/store';
|
||||
import { checkHealth } from '../lib/api';
|
||||
import { checkHealth, fetchSpeechHealth } from '../lib/api';
|
||||
|
||||
function Section({ title, children }: { title: string; children: React.ReactNode }) {
|
||||
return (
|
||||
@@ -57,10 +58,14 @@ export function SettingsPage() {
|
||||
const conversations = useAppStore((s) => s.conversations);
|
||||
const serverInfo = useAppStore((s) => s.serverInfo);
|
||||
const [healthy, setHealthy] = useState<boolean | null>(null);
|
||||
const [speechBackendAvailable, setSpeechBackendAvailable] = useState<boolean | null>(null);
|
||||
const [saved, setSaved] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
checkHealth().then(setHealthy);
|
||||
fetchSpeechHealth()
|
||||
.then((h) => setSpeechBackendAvailable(h.available))
|
||||
.catch(() => setSpeechBackendAvailable(false));
|
||||
}, []);
|
||||
|
||||
const showSaved = () => {
|
||||
@@ -225,6 +230,50 @@ export function SettingsPage() {
|
||||
</SettingRow>
|
||||
</Section>
|
||||
|
||||
{/* Speech */}
|
||||
<Section title="Speech">
|
||||
<SettingRow label="Speech-to-Text" description="Enable microphone input for voice dictation">
|
||||
<button
|
||||
onClick={() => { updateSettings({ speechEnabled: !settings.speechEnabled }); showSaved(); }}
|
||||
className="relative w-11 h-6 rounded-full transition-colors cursor-pointer"
|
||||
style={{
|
||||
background: settings.speechEnabled ? 'var(--color-accent)' : 'var(--color-bg-tertiary)',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="absolute top-0.5 left-0.5 w-5 h-5 rounded-full transition-transform bg-white"
|
||||
style={{
|
||||
transform: settings.speechEnabled ? 'translateX(20px)' : 'translateX(0)',
|
||||
boxShadow: '0 1px 3px rgba(0,0,0,0.2)',
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
</SettingRow>
|
||||
<SettingRow label="Backend status" description="Requires Whisper, Deepgram, or another speech backend">
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="w-2 h-2 rounded-full"
|
||||
style={{
|
||||
background: speechBackendAvailable === true ? 'var(--color-success)'
|
||||
: speechBackendAvailable === false ? 'var(--color-text-tertiary)'
|
||||
: 'var(--color-text-tertiary)',
|
||||
}}
|
||||
/>
|
||||
<span className="text-xs" style={{ color: 'var(--color-text-secondary)' }}>
|
||||
{speechBackendAvailable === null ? 'Checking...'
|
||||
: speechBackendAvailable ? 'Available'
|
||||
: 'Not configured'}
|
||||
</span>
|
||||
</div>
|
||||
</SettingRow>
|
||||
{!speechBackendAvailable && speechBackendAvailable !== null && (
|
||||
<div className="text-xs mt-2 px-1" style={{ color: 'var(--color-text-tertiary)' }}>
|
||||
Set up a speech backend to use voice input.
|
||||
See the <a href="https://hazyresearch.stanford.edu/OpenJarvis/user-guide/tools/" target="_blank" rel="noopener noreferrer" style={{ color: 'var(--color-accent)' }}>documentation</a> for details.
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
|
||||
{/* Data */}
|
||||
<Section title="Data">
|
||||
<SettingRow label="Conversations" description={`${conversations.length} stored locally`}>
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ theme:
|
||||
name: material
|
||||
language: en
|
||||
font:
|
||||
text: Inter
|
||||
text: Merriweather
|
||||
code: JetBrains Mono
|
||||
features:
|
||||
- navigation.tabs
|
||||
|
||||
Reference in New Issue
Block a user