Compare commits

...

2 Commits

Author SHA1 Message Date
fa70247895 Add consumer file uploading to conversation 2026-07-02 15:24:10 -04:00
d7c602603d Add server WebUI with authentication 2026-07-02 12:12:20 -04:00
51 changed files with 3890 additions and 14 deletions

189
Design/DESIGN.md Normal file
View File

@ -0,0 +1,189 @@
---
name: Kinetic Logic
colors:
surface: '#f7f9fb'
surface-dim: '#d8dadc'
surface-bright: '#f7f9fb'
surface-container-lowest: '#ffffff'
surface-container-low: '#f2f4f6'
surface-container: '#eceef0'
surface-container-high: '#e6e8ea'
surface-container-highest: '#e0e3e5'
on-surface: '#191c1e'
on-surface-variant: '#424754'
inverse-surface: '#2d3133'
inverse-on-surface: '#eff1f3'
outline: '#727785'
outline-variant: '#c2c6d6'
surface-tint: '#005ac2'
primary: '#0058be'
on-primary: '#ffffff'
primary-container: '#2170e4'
on-primary-container: '#fefcff'
inverse-primary: '#adc6ff'
secondary: '#505f76'
on-secondary: '#ffffff'
secondary-container: '#d0e1fb'
on-secondary-container: '#54647a'
tertiary: '#924700'
on-tertiary: '#ffffff'
tertiary-container: '#b75b00'
on-tertiary-container: '#fffbff'
error: '#ba1a1a'
on-error: '#ffffff'
error-container: '#ffdad6'
on-error-container: '#93000a'
primary-fixed: '#d8e2ff'
primary-fixed-dim: '#adc6ff'
on-primary-fixed: '#001a42'
on-primary-fixed-variant: '#004395'
secondary-fixed: '#d3e4fe'
secondary-fixed-dim: '#b7c8e1'
on-secondary-fixed: '#0b1c30'
on-secondary-fixed-variant: '#38485d'
tertiary-fixed: '#ffdcc6'
tertiary-fixed-dim: '#ffb786'
on-tertiary-fixed: '#311400'
on-tertiary-fixed-variant: '#723600'
background: '#f7f9fb'
on-background: '#191c1e'
surface-variant: '#e0e3e5'
typography:
headline-lg:
fontFamily: Inter
fontSize: 32px
fontWeight: '600'
lineHeight: '1.2'
letterSpacing: -0.02em
headline-md:
fontFamily: Inter
fontSize: 24px
fontWeight: '600'
lineHeight: '1.3'
letterSpacing: -0.01em
body-lg:
fontFamily: Inter
fontSize: 18px
fontWeight: '400'
lineHeight: '1.6'
body-md:
fontFamily: Inter
fontSize: 16px
fontWeight: '400'
lineHeight: '1.5'
label-sm:
fontFamily: JetBrains Mono
fontSize: 13px
fontWeight: '500'
lineHeight: '1'
letterSpacing: 0.05em
headline-lg-mobile:
fontFamily: Inter
fontSize: 26px
fontWeight: '600'
lineHeight: '1.2'
rounded:
sm: 0.125rem
DEFAULT: 0.25rem
md: 0.375rem
lg: 0.5rem
xl: 0.75rem
full: 9999px
spacing:
base: 4px
unit-1: 4px
unit-2: 8px
unit-4: 16px
unit-6: 24px
unit-8: 32px
unit-12: 48px
container-max: 1440px
sidebar-width: 260px
---
## Brand & Style
The design system focuses on precision, clarity, and the efficient management of artificial intelligence. The target audience includes developers and system administrators who require a high-utility interface that minimizes cognitive load while providing deep technical control.
The style is **Minimalistic Modern**, characterized by:
- **Functional Clarity:** Generous whitespace and a strict adherence to a grid ensure that complex data remains legible.
- **Subtle Depth:** Depth is communicated through hairline borders and soft, atmospheric shadows rather than heavy skeuomorphism.
- **Focus-Driven:** The UI recedes into the background, allowing the active "Agent" tasks and logs to remain the focal point.
- **Reliability:** A systematic approach to components conveys a sense of stability and institutional trust.
## Colors
The palette is anchored by "Agent Blue," a high-vibrancy primary color used exclusively for intent and action.
### Light Mode
- **Background:** Primary background is `#ffffff`. Secondary containers use `#f8fafc`.
- **Text:** Primary text at `#0f172a`, secondary at `#475569`.
- **Borders:** Low-contrast `#e2e8f0` for structural separation.
### Dark Mode
- **Background:** Deep charcoal `#0f172a` as the base, with `#1e293b` for elevated cards.
- **Text:** High-contrast `#f1f5f9` for readability, `#94a3b8` for metadata.
- **Borders:** Subtle `#334155` to define boundaries without visual noise.
### Functional Colors
- **Success:** `#22c55e`
- **Error:** `#ef4444`
- **Warning:** `#f59e0b`
## Typography
This design system utilizes **Inter** for all UI elements to ensure maximum legibility across high-density data views. **JetBrains Mono** is introduced for labels, status indicators, and agent logs to provide a distinct "technical" feel and distinguish machine-generated content from the interface.
- **Weight Scaling:** Use Semibold (600) for hierarchy and Regular (400) for content. Avoid Bold (700) to maintain a clean aesthetic.
- **Monospace Integration:** Use the label-sm style for version numbers, IDs, and terminal-style outputs.
- **Contrast:** Maintain a minimum 4.5:1 contrast ratio for all body text against its respective background.
## Layout & Spacing
The layout utilizes a **12-column fluid grid** for the main content area, paired with a fixed-width navigation sidebar.
- **Grid Logic:** Use 24px gutters for desktop and 16px for mobile.
- **Sidebar:** The navigation resides on the left, using a background color one step offset from the main canvas (`#f1f5f9` in light mode).
- **Safe Areas:** Maintain a 32px outer margin on desktop and 16px on mobile.
- **Vertical Rhythm:** Components should be separated by increments of 8px (unit-2) to ensure a consistent flow.
## Elevation & Depth
Hierarchy is established through **Tonal Layers** supplemented by **Ambient Shadows**.
- **Level 0 (Base):** The main application canvas.
- **Level 1 (Cards):** Elevated via a 1px border. In light mode, a soft shadow is applied: `0 1px 3px rgba(0,0,0,0.05), 0 1px 2px rgba(0,0,0,0.02)`.
- **Level 2 (Dropdowns/Modals):** Increased shadow depth: `0 10px 15px -3px rgba(0,0,0,0.1)`.
- **Interaction:** On hover, cards may transition to a slightly darker border color rather than increasing shadow depth to maintain the minimalistic feel.
## Shapes
The shape language is "Soft" (0.25rem / 4px base), providing a professional and geometric appearance that avoids the playfulness of highly rounded "pill" designs.
- **Buttons & Inputs:** Use the base `rounded` (4px).
- **Cards & Sections:** Use `rounded-lg` (8px).
- **Focus Rings:** Use a 2px offset with a 2px solid Agent Blue stroke to ensure high visibility.
## Components
### Buttons
- **Primary:** Solid Agent Blue background, white text. No gradient.
- **Secondary:** Transparent background, 1px border (`#e2e8f0` light / `#334155` dark), primary text color.
- **States:** Active/Pressed states shift the background color 10% darker. Focus states use the 2px offset ring.
### Input Fields
- **Default:** White background (light) or `#1e293b` (dark). 1px border.
- **Focus:** Border transitions to Agent Blue. Label moves to a "floating" position or stays strictly above; never disappears.
- **Validation:** Error states replace the blue border with `#ef4444`.
### Navigation Sidebar
- **Links:** Transparent background by default. Active state uses a light tint of Agent Blue with a 3px vertical "indicator" bar on the far left.
- **Icons:** Use 20px stroke-based icons for clarity.
### Cards
- **Structure:** 1px border with a padding of `unit-6` (24px).
- **Header:** Optional 1px bottom border to separate title from content.
### Chips/Status Badges
- **Style:** Small, all-caps, mono font. Used for "Agent Status" (Running, Idle, Error).
- **Colors:** Subtle background tints (e.g., 10% opacity green for "Running").

305
Design/admins_list.html Normal file
View File

@ -0,0 +1,305 @@
<!DOCTYPE html>
<html class="dark" lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>System Admins Management - Henry</title>
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com" rel="preconnect"/>
<link crossorigin="" href="https://fonts.gstatic.com" rel="preconnect"/>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;900&amp;family=JetBrains+Mono:wght@500&amp;display=swap" rel="stylesheet"/>
<!-- Material Symbols -->
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"secondary": "#505f76",
"on-secondary-fixed-variant": "#38485d",
"on-primary-fixed-variant": "#004395",
"on-error": "#ffffff",
"surface-container": "#eceef0",
"surface-bright": "#f7f9fb",
"on-tertiary": "#ffffff",
"surface-dim": "#d8dadc",
"on-primary": "#ffffff",
"surface-container-highest": "#e0e3e5",
"tertiary-fixed": "#ffdcc6",
"tertiary-fixed-dim": "#ffb786",
"on-surface-variant": "#424754",
"on-primary-container": "#fefcff",
"primary-fixed": "#d8e2ff",
"surface-container-lowest": "#ffffff",
"inverse-on-surface": "#eff1f3",
"surface": "#f7f9fb",
"on-tertiary-container": "#fffbff",
"primary-container": "#2170e4",
"on-secondary-container": "#54647a",
"on-tertiary-fixed": "#311400",
"secondary-fixed-dim": "#b7c8e1",
"secondary-fixed": "#d3e4fe",
"primary-fixed-dim": "#adc6ff",
"outline-variant": "#c2c6d6",
"on-secondary": "#ffffff",
"inverse-surface": "#2d3133",
"on-primary-fixed": "#001a42",
"error-container": "#ffdad6",
"inverse-primary": "#adc6ff",
"tertiary-container": "#b75b00",
"background": "#f7f9fb",
"tertiary": "#924700",
"on-tertiary-fixed-variant": "#723600",
"on-secondary-fixed": "#0b1c30",
"on-error-container": "#93000a",
"surface-tint": "#005ac2",
"surface-container-low": "#f2f4f6",
"on-background": "#191c1e",
"outline": "#727785",
"surface-container-high": "#e6e8ea",
"surface-variant": "#e0e3e5",
"on-surface": "#191c1e",
"error": "#ba1a1a",
"secondary-container": "#d0e1fb",
"primary": "#0058be"
},
"borderRadius": {
"DEFAULT": "0.125rem",
"lg": "0.25rem",
"xl": "0.5rem",
"full": "0.75rem"
},
"spacing": {
"unit-2": "8px",
"base": "4px",
"unit-4": "16px",
"unit-6": "24px",
"container-max": "1440px",
"unit-1": "4px",
"sidebar-width": "260px",
"unit-12": "48px",
"unit-8": "32px"
},
"fontFamily": {
"label-sm": ["JetBrains Mono"],
"headline-md": ["Inter"],
"body-md": ["Inter"],
"headline-lg-mobile": ["Inter"],
"headline-lg": ["Inter"],
"body-lg": ["Inter"]
},
"fontSize": {
"label-sm": ["13px", { "lineHeight": "1", "letterSpacing": "0.05em", "fontWeight": "500" }],
"headline-md": ["24px", { "lineHeight": "1.3", "letterSpacing": "-0.01em", "fontWeight": "600" }],
"body-md": ["16px", { "lineHeight": "1.5", "fontWeight": "400" }],
"headline-lg-mobile": ["26px", { "lineHeight": "1.2", "fontWeight": "600" }],
"headline-lg": ["32px", { "lineHeight": "1.2", "letterSpacing": "-0.02em", "fontWeight": "600" }],
"body-lg": ["18px", { "lineHeight": "1.6", "fontWeight": "400" }]
}
}
}
}
</script>
<style>
body { background-color: #191c1e; color: #f7f9fb; } /* Base dark mode enforcing due to strict JSON */
</style>
</head>
<body class="flex min-h-screen bg-inverse-surface text-surface-bright selection:bg-primary-container selection:text-on-primary-container">
<!-- SideNavBar (from JSON) -->
<nav class="hidden md:flex w-sidebar-width h-screen fixed left-0 top-0 border-r border-outline-variant dark:border-on-surface-variant bg-surface-container-low dark:bg-on-secondary-fixed-variant flex-col py-unit-6 h-full z-50">
<!-- Header -->
<div class="px-unit-6 mb-unit-8">
<div class="flex items-center gap-unit-4">
<div class="w-8 h-8 rounded-lg bg-primary-container flex items-center justify-center shrink-0">
<span class="material-symbols-outlined text-on-primary-container text-[20px]">smart_toy</span>
</div>
<div>
<h1 class="text-headline-md font-headline-md font-black text-primary dark:text-primary-fixed-dim">Henry Admin</h1>
<p class="font-label-sm text-label-sm text-on-surface-variant dark:text-surface-variant mt-1">v2.4.0-stable</p>
</div>
</div>
</div>
<!-- Main Navigation -->
<div class="flex-1 px-unit-4 space-y-unit-2">
<a class="flex items-center gap-unit-4 px-unit-4 py-3 rounded-lg text-on-surface-variant dark:text-surface-variant hover:text-on-surface dark:hover:text-surface-bright hover:bg-surface-container-high dark:hover:bg-on-secondary-fixed transition-all duration-200 ease-in-out" href="#">
<span class="material-symbols-outlined">smart_toy</span>
<span class="font-label-sm text-label-sm">Agents</span>
</a>
<a class="flex items-center gap-unit-4 px-unit-4 py-3 rounded-lg text-on-surface-variant dark:text-surface-variant hover:text-on-surface dark:hover:text-surface-bright hover:bg-surface-container-high dark:hover:bg-on-secondary-fixed transition-all duration-200 ease-in-out" href="#">
<span class="material-symbols-outlined">history</span>
<span class="font-label-sm text-label-sm">History</span>
</a>
<!-- Active Tab -->
<a class="flex items-center gap-unit-4 px-unit-4 py-3 rounded-lg bg-secondary-container dark:bg-on-secondary-fixed-variant text-primary dark:text-primary-fixed-dim border-l-4 border-primary dark:border-primary-fixed-dim font-bold transition-all duration-200 ease-in-out" href="#">
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 1;">admin_panel_settings</span>
<span class="font-label-sm text-label-sm">Admins</span>
</a>
<a class="flex items-center gap-unit-4 px-unit-4 py-3 rounded-lg text-on-surface-variant dark:text-surface-variant hover:text-on-surface dark:hover:text-surface-bright hover:bg-surface-container-high dark:hover:bg-on-secondary-fixed transition-all duration-200 ease-in-out" href="#">
<span class="material-symbols-outlined">settings</span>
<span class="font-label-sm text-label-sm">Settings</span>
</a>
<a class="flex items-center gap-unit-4 px-unit-4 py-3 rounded-lg text-on-surface-variant dark:text-surface-variant hover:text-on-surface dark:hover:text-surface-bright hover:bg-surface-container-high dark:hover:bg-on-secondary-fixed transition-all duration-200 ease-in-out" href="#">
<span class="material-symbols-outlined">description</span>
<span class="font-label-sm text-label-sm">Documentation</span>
</a>
</div>
<!-- Footer Navigation -->
<div class="px-unit-4 mt-auto space-y-unit-2 border-t border-outline-variant dark:border-on-surface-variant pt-unit-4">
<a class="flex items-center gap-unit-4 px-unit-4 py-3 rounded-lg text-on-surface-variant dark:text-surface-variant hover:text-on-surface dark:hover:text-surface-bright hover:bg-surface-container-high dark:hover:bg-on-secondary-fixed transition-all duration-200 ease-in-out" href="#">
<span class="material-symbols-outlined">contact_support</span>
<span class="font-label-sm text-label-sm">Support</span>
</a>
<a class="flex items-center gap-unit-4 px-unit-4 py-3 rounded-lg text-on-surface-variant dark:text-surface-variant hover:text-on-surface dark:hover:text-surface-bright hover:bg-surface-container-high dark:hover:bg-on-secondary-fixed transition-all duration-200 ease-in-out" href="#">
<span class="material-symbols-outlined">logout</span>
<span class="font-label-sm text-label-sm">Log Out</span>
</a>
</div>
</nav>
<!-- Main Content Wrapper -->
<div class="flex-1 flex flex-col md:ml-sidebar-width min-h-screen">
<!-- TopNavBar (from JSON) -->
<header class="flex justify-between items-center px-unit-8 w-full sticky top-0 z-40 h-16 bg-surface-bright dark:bg-inverse-surface border-b border-outline-variant dark:border-on-surface-variant shadow-sm">
<!-- Left Area: Brand & Search -->
<div class="flex items-center gap-unit-8">
<div class="md:hidden">
<span class="text-headline-md font-headline-md font-bold text-primary dark:text-primary-fixed-dim">Henry</span>
</div>
<!-- Search Bar -->
<div class="relative hidden sm:block">
<span class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant dark:text-surface-variant text-[20px]">search</span>
<input class="pl-10 pr-4 py-2 w-64 bg-surface-container dark:bg-on-secondary-fixed-variant border border-outline-variant dark:border-on-surface-variant rounded-full font-body-md text-body-md text-on-surface dark:text-surface-bright focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary transition-colors" placeholder="Search administrators..." type="text"/>
</div>
</div>
<!-- Right Area: Actions & Profile -->
<div class="flex items-center gap-unit-6">
<button class="text-on-surface-variant dark:text-surface-variant hover:bg-surface-container dark:hover:bg-on-secondary-fixed-variant p-2 rounded-full transition-colors cursor-pointer active:opacity-80 flex items-center justify-center">
<span class="material-symbols-outlined">notifications</span>
</button>
<button class="text-on-surface-variant dark:text-surface-variant hover:bg-surface-container dark:hover:bg-on-secondary-fixed-variant p-2 rounded-full transition-colors cursor-pointer active:opacity-80 flex items-center justify-center">
<span class="material-symbols-outlined">help_outline</span>
</button>
<div class="w-8 h-8 rounded-full bg-primary-container border border-outline-variant dark:border-on-surface-variant overflow-hidden cursor-pointer active:opacity-80 transition-all">
<img alt="Administrator Profile" class="w-full h-full object-cover" data-alt="A highly detailed close-up portrait of a professional system administrator in a high-tech server room environment, softly lit by blue screen glows. Minimalist corporate aesthetic, high resolution." src="https://lh3.googleusercontent.com/aida-public/AB6AXuDTGHeDAgRFqcjBSgSrvNSlVlnBFUkb70D6eJFvkA-Hd8MmDLb9ssqkxwL4_A8Ob9xrQ45jtuJ7J9bH8CcaIaKeXEOaV0wJ_vLq1cduHmGS2pgdyM6M-Fxluv46ida9lh0dODXtmqM65xHhwd5b-uVyAmZ9M7lQet3YuFbWHgJnBGE_AKnwm4YzD7roPhHoULibvOmCbfdwAkMXIRRCAMz7zKKltynpqDrgqPKHxgUbyQHSNZVyaTZlN9_IndZ5WPorVfK-z4X26IkL"/>
</div>
</div>
</header>
<!-- Page Canvas -->
<main class="flex-1 p-unit-4 md:p-unit-8 max-w-container-max mx-auto w-full">
<!-- Page Header -->
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-unit-4 mb-unit-8">
<div>
<h2 class="font-headline-lg text-headline-lg-mobile md:text-headline-lg text-on-surface dark:text-surface-bright">System Admins</h2>
<p class="font-body-md text-body-md text-on-surface-variant dark:text-surface-variant mt-1">Manage platform access and administrative privileges.</p>
</div>
<button class="bg-primary text-on-primary hover:bg-primary/90 font-label-sm text-label-sm px-unit-6 py-3 rounded-lg flex items-center gap-unit-2 transition-colors duration-200 shadow-sm self-start sm:self-auto">
<span class="material-symbols-outlined text-[18px]">person_add</span>
Invite Admin
</button>
</div>
<!-- Data Table Card -->
<div class="bg-surface-bright dark:bg-[#1f2224] border border-outline-variant dark:border-on-surface-variant rounded-xl shadow-sm overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full text-left border-collapse">
<thead>
<tr class="border-b border-outline-variant dark:border-on-surface-variant bg-surface-container-low dark:bg-on-secondary-fixed-variant/50">
<th class="py-unit-4 px-unit-6 font-label-sm text-label-sm text-on-surface-variant dark:text-surface-variant uppercase tracking-wider">Administrator</th>
<th class="py-unit-4 px-unit-6 font-label-sm text-label-sm text-on-surface-variant dark:text-surface-variant uppercase tracking-wider">Role</th>
<th class="py-unit-4 px-unit-6 font-label-sm text-label-sm text-on-surface-variant dark:text-surface-variant uppercase tracking-wider">Last Login</th>
<th class="py-unit-4 px-unit-6 font-label-sm text-label-sm text-on-surface-variant dark:text-surface-variant uppercase tracking-wider text-right">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-outline-variant dark:divide-on-surface-variant">
<!-- Row 1 -->
<tr class="hover:bg-surface-container-lowest dark:hover:bg-[#25282a] transition-colors group">
<td class="py-unit-4 px-unit-6">
<div class="flex items-center gap-unit-4">
<div class="w-10 h-10 rounded-full bg-secondary-container dark:bg-on-secondary-fixed-variant flex items-center justify-center text-primary dark:text-primary-fixed-dim font-headline-md text-headline-md">
E
</div>
<div>
<p class="font-body-md text-body-md font-semibold text-on-surface dark:text-surface-bright">Elena Rostova</p>
<p class="font-body-md text-body-md text-sm text-on-surface-variant dark:text-surface-variant">elena.r@henry.ai</p>
</div>
</div>
</td>
<td class="py-unit-4 px-unit-6">
<span class="inline-flex items-center px-2 py-1 rounded-md bg-primary-container/20 text-primary-fixed-dim border border-primary/20 font-label-sm text-label-sm uppercase">Super Admin</span>
</td>
<td class="py-unit-4 px-unit-6 font-body-md text-body-md text-on-surface-variant dark:text-surface-variant">
2 mins ago
</td>
<td class="py-unit-4 px-unit-6 text-right">
<button class="text-on-surface-variant dark:text-surface-variant hover:text-primary dark:hover:text-primary-fixed-dim p-2 rounded-full transition-colors opacity-0 group-hover:opacity-100 focus:opacity-100">
<span class="material-symbols-outlined text-[20px]">edit</span>
</button>
</td>
</tr>
<!-- Row 2 -->
<tr class="hover:bg-surface-container-lowest dark:hover:bg-[#25282a] transition-colors group">
<td class="py-unit-4 px-unit-6">
<div class="flex items-center gap-unit-4">
<div class="w-10 h-10 rounded-full bg-secondary-container dark:bg-on-secondary-fixed-variant flex items-center justify-center text-primary dark:text-primary-fixed-dim font-headline-md text-headline-md">
M
</div>
<div>
<p class="font-body-md text-body-md font-semibold text-on-surface dark:text-surface-bright">Marcus Chen</p>
<p class="font-body-md text-body-md text-sm text-on-surface-variant dark:text-surface-variant">m.chen@henry.ai</p>
</div>
</div>
</td>
<td class="py-unit-4 px-unit-6">
<span class="inline-flex items-center px-2 py-1 rounded-md bg-surface-container-high dark:bg-on-secondary-fixed text-on-surface-variant dark:text-surface-variant border border-outline-variant/30 font-label-sm text-label-sm uppercase">Security Ops</span>
</td>
<td class="py-unit-4 px-unit-6 font-body-md text-body-md text-on-surface-variant dark:text-surface-variant">
Oct 12, 14:30
</td>
<td class="py-unit-4 px-unit-6 text-right">
<button class="text-on-surface-variant dark:text-surface-variant hover:text-primary dark:hover:text-primary-fixed-dim p-2 rounded-full transition-colors opacity-0 group-hover:opacity-100 focus:opacity-100">
<span class="material-symbols-outlined text-[20px]">edit</span>
</button>
</td>
</tr>
<!-- Row 3 -->
<tr class="hover:bg-surface-container-lowest dark:hover:bg-[#25282a] transition-colors group">
<td class="py-unit-4 px-unit-6">
<div class="flex items-center gap-unit-4">
<div class="w-10 h-10 rounded-full bg-secondary-container dark:bg-on-secondary-fixed-variant flex items-center justify-center text-primary dark:text-primary-fixed-dim font-headline-md text-headline-md">
S
</div>
<div>
<p class="font-body-md text-body-md font-semibold text-on-surface dark:text-surface-bright">Sarah Jenkins</p>
<p class="font-body-md text-body-md text-sm text-on-surface-variant dark:text-surface-variant">s.jenkins@henry.ai</p>
</div>
</div>
</td>
<td class="py-unit-4 px-unit-6">
<span class="inline-flex items-center px-2 py-1 rounded-md bg-surface-container-high dark:bg-on-secondary-fixed text-on-surface-variant dark:text-surface-variant border border-outline-variant/30 font-label-sm text-label-sm uppercase">Agent Configurator</span>
</td>
<td class="py-unit-4 px-unit-6 font-body-md text-body-md text-on-surface-variant dark:text-surface-variant">
Oct 10, 09:15
</td>
<td class="py-unit-4 px-unit-6 text-right">
<button class="text-on-surface-variant dark:text-surface-variant hover:text-primary dark:hover:text-primary-fixed-dim p-2 rounded-full transition-colors opacity-0 group-hover:opacity-100 focus:opacity-100">
<span class="material-symbols-outlined text-[20px]">edit</span>
</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Pagination / Footer Area -->
<div class="py-unit-4 px-unit-6 border-t border-outline-variant dark:border-on-surface-variant flex items-center justify-between bg-surface-container-lowest dark:bg-inverse-surface">
<span class="font-label-sm text-label-sm text-on-surface-variant dark:text-surface-variant">Showing 1 to 3 of 12 entries</span>
<div class="flex gap-unit-2">
<button class="px-3 py-1 rounded border border-outline-variant dark:border-on-surface-variant text-on-surface-variant dark:text-surface-variant hover:bg-surface-container dark:hover:bg-on-secondary-fixed-variant transition-colors disabled:opacity-50" disabled="">Prev</button>
<button class="px-3 py-1 rounded border border-outline-variant dark:border-on-surface-variant text-on-surface-variant dark:text-surface-variant hover:bg-surface-container dark:hover:bg-on-secondary-fixed-variant transition-colors">Next</button>
</div>
</div>
</div>
</main>
</div>
</body></html>

411
Design/conversations.html Normal file
View File

@ -0,0 +1,411 @@
<!DOCTYPE html>
<html class="dark" lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>Henry - Conversation Hub</title>
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com" rel="preconnect"/>
<link crossorigin="" href="https://fonts.gstatic.com" rel="preconnect"/>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&amp;family=JetBrains+Mono:wght@400;500&amp;display=swap" rel="stylesheet"/>
<!-- Material Symbols -->
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<!-- Tailwind Config -->
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"secondary": "#505f76",
"on-secondary-fixed-variant": "#38485d",
"on-primary-fixed-variant": "#004395",
"on-error": "#ffffff",
"surface-container": "#eceef0",
"surface-bright": "#f7f9fb",
"on-tertiary": "#ffffff",
"surface-dim": "#d8dadc",
"on-primary": "#ffffff",
"surface-container-highest": "#e0e3e5",
"tertiary-fixed": "#ffdcc6",
"tertiary-fixed-dim": "#ffb786",
"on-surface-variant": "#424754",
"on-primary-container": "#fefcff",
"primary-fixed": "#d8e2ff",
"surface-container-lowest": "#ffffff",
"inverse-on-surface": "#eff1f3",
"surface": "#f7f9fb",
"on-tertiary-container": "#fffbff",
"primary-container": "#2170e4",
"on-secondary-container": "#54647a",
"on-tertiary-fixed": "#311400",
"secondary-fixed-dim": "#b7c8e1",
"secondary-fixed": "#d3e4fe",
"primary-fixed-dim": "#adc6ff",
"outline-variant": "#c2c6d6",
"on-secondary": "#ffffff",
"inverse-surface": "#2d3133",
"on-primary-fixed": "#001a42",
"error-container": "#ffdad6",
"inverse-primary": "#adc6ff",
"tertiary-container": "#b75b00",
"background": "#f7f9fb",
"tertiary": "#924700",
"on-tertiary-fixed-variant": "#723600",
"on-secondary-fixed": "#0b1c30",
"on-error-container": "#93000a",
"surface-tint": "#005ac2",
"surface-container-low": "#f2f4f6",
"on-background": "#191c1e",
"outline": "#727785",
"surface-container-high": "#e6e8ea",
"surface-variant": "#e0e3e5",
"on-surface": "#191c1e",
"error": "#ba1a1a",
"secondary-container": "#d0e1fb",
"primary": "#0058be"
},
"borderRadius": {
"DEFAULT": "0.125rem",
"lg": "0.25rem",
"xl": "0.5rem",
"full": "0.75rem"
},
"spacing": {
"unit-2": "8px",
"base": "4px",
"unit-4": "16px",
"unit-6": "24px",
"container-max": "1440px",
"unit-1": "4px",
"sidebar-width": "260px",
"unit-12": "48px",
"unit-8": "32px"
},
"fontFamily": {
"label-sm": ["JetBrains Mono"],
"headline-md": ["Inter"],
"body-md": ["Inter"],
"headline-lg-mobile": ["Inter"],
"headline-lg": ["Inter"],
"body-lg": ["Inter"]
},
"fontSize": {
"label-sm": ["13px", { "lineHeight": "1", "letterSpacing": "0.05em", "fontWeight": "500" }],
"headline-md": ["24px", { "lineHeight": "1.3", "letterSpacing": "-0.01em", "fontWeight": "600" }],
"body-md": ["16px", { "lineHeight": "1.5", "fontWeight": "400" }],
"headline-lg-mobile": ["26px", { "lineHeight": "1.2", "fontWeight": "600" }],
"headline-lg": ["32px", { "lineHeight": "1.2", "letterSpacing": "-0.02em", "fontWeight": "600" }],
"body-lg": ["18px", { "lineHeight": "1.6", "fontWeight": "400" }]
}
}
}
}
</script>
<style>
/* Custom scrollbar for webkit */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #38485d; border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: #505f76; }
/* Utility for Material Symbols */
.material-symbols-outlined { font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24; }
.icon-fill { font-variation-settings: 'FILL' 1; }
/* Glassmorphism utility for cards */
.glass-panel {
background: rgba(56, 72, 93, 0.4);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(194, 198, 214, 0.1);
}
</style>
</head>
<body class="bg-surface-bright dark:bg-on-secondary-fixed text-on-surface dark:text-surface-bright h-screen w-full flex overflow-hidden font-body-md selection:bg-primary-container selection:text-on-primary-container">
<!-- Shared Component: SideNavBar -->
<nav class="bg-surface-container-low dark:bg-on-secondary-fixed-variant w-sidebar-width h-screen fixed left-0 top-0 border-r border-outline-variant dark:border-on-surface-variant flex flex-col py-unit-6 h-full z-30 transition-all duration-200 ease-in-out md:flex hidden">
<!-- Header -->
<div class="px-unit-6 mb-unit-8 flex items-center gap-3">
<div class="w-10 h-10 rounded-lg bg-primary flex items-center justify-center text-on-primary shadow-sm">
<span class="material-symbols-outlined icon-fill">robot_2</span>
</div>
<div>
<h1 class="text-headline-md font-headline-md font-black text-primary dark:text-primary-fixed-dim tracking-tight">Henry Admin</h1>
<p class="text-label-sm font-label-sm text-on-surface-variant dark:text-surface-variant mt-1">v2.4.0-stable</p>
</div>
</div>
<!-- Main Navigation -->
<div class="flex-1 px-unit-4 flex flex-col gap-unit-1">
<!-- Inactive Tab -->
<a class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-on-surface-variant dark:text-surface-variant hover:text-on-surface dark:hover:text-surface-bright hover:bg-surface-container-high dark:hover:bg-on-secondary-fixed transition-all duration-200 ease-in-out group" href="#">
<span class="material-symbols-outlined text-[20px] group-hover:scale-110 transition-transform">smart_toy</span>
<span class="font-body-md text-body-md">Agents</span>
</a>
<!-- Active Tab (History / Conversation Hub context) -->
<a class="flex items-center gap-3 px-3 py-2.5 rounded-lg bg-secondary-container dark:bg-on-secondary-fixed-variant text-primary dark:text-primary-fixed-dim border-l-4 border-primary dark:border-primary-fixed-dim font-bold transition-all duration-200 ease-in-out" href="#">
<span class="material-symbols-outlined text-[20px] icon-fill">history</span>
<span class="font-body-md text-body-md">History</span>
</a>
<!-- Inactive Tabs -->
<a class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-on-surface-variant dark:text-surface-variant hover:text-on-surface dark:hover:text-surface-bright hover:bg-surface-container-high dark:hover:bg-on-secondary-fixed transition-all duration-200 ease-in-out group" href="#">
<span class="material-symbols-outlined text-[20px] group-hover:scale-110 transition-transform">admin_panel_settings</span>
<span class="font-body-md text-body-md">Admins</span>
</a>
<a class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-on-surface-variant dark:text-surface-variant hover:text-on-surface dark:hover:text-surface-bright hover:bg-surface-container-high dark:hover:bg-on-secondary-fixed transition-all duration-200 ease-in-out group" href="#">
<span class="material-symbols-outlined text-[20px] group-hover:scale-110 transition-transform">settings</span>
<span class="font-body-md text-body-md">Settings</span>
</a>
<a class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-on-surface-variant dark:text-surface-variant hover:text-on-surface dark:hover:text-surface-bright hover:bg-surface-container-high dark:hover:bg-on-secondary-fixed transition-all duration-200 ease-in-out group" href="#">
<span class="material-symbols-outlined text-[20px] group-hover:scale-110 transition-transform">description</span>
<span class="font-body-md text-body-md">Documentation</span>
</a>
</div>
<!-- Footer Navigation -->
<div class="px-unit-4 mt-auto flex flex-col gap-unit-1 pt-unit-4 border-t border-outline-variant dark:border-on-surface-variant/50">
<a class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-on-surface-variant dark:text-surface-variant hover:text-on-surface dark:hover:text-surface-bright hover:bg-surface-container-high dark:hover:bg-on-secondary-fixed transition-all duration-200 ease-in-out group" href="#">
<span class="material-symbols-outlined text-[20px] group-hover:scale-110 transition-transform">contact_support</span>
<span class="font-body-md text-body-md">Support</span>
</a>
<a class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-on-surface-variant dark:text-surface-variant hover:text-error dark:hover:text-error-container hover:bg-surface-container-high dark:hover:bg-on-secondary-fixed transition-all duration-200 ease-in-out group" href="#">
<span class="material-symbols-outlined text-[20px] group-hover:scale-110 transition-transform">logout</span>
<span class="font-body-md text-body-md">Log Out</span>
</a>
</div>
</nav>
<!-- Main Content Area -->
<main class="flex-1 ml-0 md:ml-sidebar-width flex flex-col h-full relative">
<!-- Header Section -->
<header class="px-unit-8 pt-unit-8 pb-unit-4 flex justify-between items-end shrink-0">
<div>
<h2 class="text-headline-lg font-headline-lg text-surface-lowest dark:text-white tracking-tight">Conversation Hub</h2>
<p class="text-body-md font-body-md text-on-surface-variant dark:text-secondary-fixed-dim mt-1 max-w-2xl">Monitor and review real-time and historical agent interactions.</p>
</div>
<div class="flex gap-unit-2">
<div class="relative group">
<span class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant dark:text-surface-variant">search</span>
<input class="pl-10 pr-4 py-2 bg-surface-container-lowest dark:bg-on-secondary-fixed-variant border border-outline-variant dark:border-on-secondary-container rounded-lg focus:outline-none focus:border-primary dark:focus:border-primary-fixed-dim focus:ring-1 focus:ring-primary dark:focus:ring-primary-fixed-dim text-body-md font-body-md w-64 transition-all dark:text-white placeholder:text-outline dark:placeholder:text-surface-variant/50" placeholder="Search User ID or Agent..." type="text"/>
</div>
<button class="flex items-center gap-2 px-4 py-2 rounded-lg bg-surface-container-lowest dark:bg-on-secondary-fixed-variant border border-outline-variant dark:border-on-secondary-container hover:bg-surface-container-low dark:hover:bg-on-secondary-fixed transition-colors text-body-md font-body-md text-on-surface dark:text-surface-bright">
<span class="material-symbols-outlined text-[20px]">filter_list</span>
Filter
</button>
</div>
</header>
<!-- Conversation Grid (Avoiding standard list, using separated cards) -->
<div class="flex-1 overflow-y-auto px-unit-8 pb-unit-8 pt-unit-2 flex flex-col gap-unit-2">
<!-- List Header (Semantic) -->
<div class="grid grid-cols-12 gap-4 px-6 py-2 text-label-sm font-label-sm text-on-surface-variant dark:text-secondary-fixed-dim uppercase tracking-wider mb-2 sticky top-0 bg-surface-bright dark:bg-on-secondary-fixed z-10">
<div class="col-span-3">Agent</div>
<div class="col-span-3">User Identifier</div>
<div class="col-span-2">Status</div>
<div class="col-span-3">Last Active</div>
<div class="col-span-1 text-right">Action</div>
</div>
<!-- Item 1 -->
<div class="glass-panel rounded-xl p-4 flex items-center group hover:border-outline-variant/30 transition-all duration-300">
<div class="grid grid-cols-12 gap-4 w-full items-center">
<div class="col-span-3 flex items-center gap-3">
<div class="w-8 h-8 rounded-full bg-primary-container/20 flex items-center justify-center text-primary-fixed-dim">
<span class="material-symbols-outlined text-[18px]">support_agent</span>
</div>
<span class="font-body-md text-body-md font-medium text-surface-lowest dark:text-white">ResolveBot Alpha</span>
</div>
<div class="col-span-3">
<span class="font-label-sm text-label-sm text-on-surface-variant dark:text-secondary-fixed-dim bg-surface-container dark:bg-on-secondary-fixed px-2 py-1 rounded">USR-9281A</span>
</div>
<div class="col-span-2">
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-primary-container/10 border border-primary-fixed-dim/20 text-primary-fixed-dim font-label-sm text-label-sm">
<span class="w-1.5 h-1.5 rounded-full bg-primary-fixed-dim animate-pulse"></span> Active
</span>
</div>
<div class="col-span-3 text-body-md font-body-md text-on-surface-variant dark:text-surface-variant">
Just now
</div>
<div class="col-span-1 flex justify-end">
<button class="opacity-0 group-hover:opacity-100 px-4 py-1.5 rounded border border-outline-variant dark:border-on-secondary-container hover:bg-surface-container-low dark:hover:bg-on-secondary-fixed-variant text-body-md font-body-md text-primary dark:text-primary-fixed-dim transition-all flex items-center gap-1 focus:opacity-100" onclick="toggleTranscript('USR-9281A', 'ResolveBot Alpha')">
View <span class="material-symbols-outlined text-[16px]">arrow_forward</span>
</button>
</div>
</div>
</div>
<!-- Item 2 -->
<div class="glass-panel rounded-xl p-4 flex items-center group hover:border-outline-variant/30 transition-all duration-300">
<div class="grid grid-cols-12 gap-4 w-full items-center">
<div class="col-span-3 flex items-center gap-3">
<div class="w-8 h-8 rounded-full bg-tertiary-container/20 flex items-center justify-center text-tertiary-fixed-dim">
<span class="material-symbols-outlined text-[18px]">shopping_cart_checkout</span>
</div>
<span class="font-body-md text-body-md font-medium text-surface-lowest dark:text-white">SalesAssistant V2</span>
</div>
<div class="col-span-3">
<span class="font-label-sm text-label-sm text-on-surface-variant dark:text-secondary-fixed-dim bg-surface-container dark:bg-on-secondary-fixed px-2 py-1 rounded">USR-4429X</span>
</div>
<div class="col-span-2">
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-surface-container-highest dark:bg-on-secondary-fixed-variant border border-outline-variant/20 text-on-surface-variant dark:text-secondary-fixed-dim font-label-sm text-label-sm">
Idle
</span>
</div>
<div class="col-span-3 text-body-md font-body-md text-on-surface-variant dark:text-surface-variant">
5 mins ago
</div>
<div class="col-span-1 flex justify-end">
<button class="opacity-0 group-hover:opacity-100 px-4 py-1.5 rounded border border-outline-variant dark:border-on-secondary-container hover:bg-surface-container-low dark:hover:bg-on-secondary-fixed-variant text-body-md font-body-md text-primary dark:text-primary-fixed-dim transition-all flex items-center gap-1 focus:opacity-100" onclick="toggleTranscript('USR-4429X', 'SalesAssistant V2')">
View <span class="material-symbols-outlined text-[16px]">arrow_forward</span>
</button>
</div>
</div>
</div>
<!-- Item 3 -->
<div class="glass-panel rounded-xl p-4 flex items-center group hover:border-outline-variant/30 transition-all duration-300">
<div class="grid grid-cols-12 gap-4 w-full items-center">
<div class="col-span-3 flex items-center gap-3">
<div class="w-8 h-8 rounded-full bg-error-container/20 flex items-center justify-center text-error-container">
<span class="material-symbols-outlined text-[18px]">warning</span>
</div>
<span class="font-body-md text-body-md font-medium text-surface-lowest dark:text-white">TechSupport Core</span>
</div>
<div class="col-span-3">
<span class="font-label-sm text-label-sm text-on-surface-variant dark:text-secondary-fixed-dim bg-surface-container dark:bg-on-secondary-fixed px-2 py-1 rounded">USR-1002C</span>
</div>
<div class="col-span-2">
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-surface-container-highest dark:bg-on-secondary-fixed-variant border border-outline-variant/20 text-on-surface-variant dark:text-secondary-fixed-dim font-label-sm text-label-sm">
Resolved
</span>
</div>
<div class="col-span-3 text-body-md font-body-md text-on-surface-variant dark:text-surface-variant">
1 hour ago
</div>
<div class="col-span-1 flex justify-end">
<button class="opacity-0 group-hover:opacity-100 px-4 py-1.5 rounded border border-outline-variant dark:border-on-secondary-container hover:bg-surface-container-low dark:hover:bg-on-secondary-fixed-variant text-body-md font-body-md text-primary dark:text-primary-fixed-dim transition-all flex items-center gap-1 focus:opacity-100" onclick="toggleTranscript('USR-1002C', 'TechSupport Core')">
View <span class="material-symbols-outlined text-[16px]">arrow_forward</span>
</button>
</div>
</div>
</div>
<!-- End of list padding -->
<div class="h-8 shrink-0"></div>
</div>
</main>
<!-- Drill-Down Side Panel (Chat Transcript) -->
<div class="fixed right-0 top-0 h-full w-[480px] bg-surface-bright dark:bg-inverse-surface border-l border-outline-variant dark:border-on-secondary-container shadow-2xl transform translate-x-full transition-transform duration-300 ease-[cubic-bezier(0.4,0,0.2,1)] z-50 flex flex-col" id="transcriptPanel">
<!-- Panel Header -->
<div class="px-6 py-5 border-b border-outline-variant dark:border-on-secondary-container flex justify-between items-center bg-surface-container-lowest dark:bg-inverse-surface shrink-0">
<div>
<h3 class="text-headline-md font-headline-md font-semibold text-surface-lowest dark:text-white" id="panelAgentName">Agent Name</h3>
<p class="text-label-sm font-label-sm text-on-surface-variant dark:text-secondary-fixed-dim mt-1 flex items-center gap-2">
Session ID: <span class="font-mono bg-surface-container dark:bg-on-secondary-fixed px-1.5 py-0.5 rounded" id="panelUserId">USR-XXXX</span>
</p>
</div>
<button class="w-8 h-8 flex items-center justify-center rounded-full hover:bg-surface-container dark:hover:bg-on-secondary-fixed-variant text-on-surface-variant dark:text-surface-variant transition-colors" onclick="closeTranscript()">
<span class="material-symbols-outlined">close</span>
</button>
</div>
<!-- Chat Transcript Area -->
<div class="flex-1 overflow-y-auto p-6 flex flex-col gap-6 bg-[#f8fafc] dark:bg-[#1a2332]">
<!-- Timestamp Divider -->
<div class="flex items-center justify-center">
<span class="text-label-sm font-label-sm text-on-surface-variant dark:text-secondary-fixed-dim px-3 py-1 rounded-full bg-surface-container-lowest dark:bg-on-secondary-fixed-variant border border-outline-variant/30">Today, 14:32</span>
</div>
<!-- User Message -->
<div class="flex flex-col items-end gap-1">
<div class="bg-primary dark:bg-primary-container text-on-primary dark:text-on-primary-container px-4 py-3 rounded-2xl rounded-tr-sm max-w-[85%] shadow-sm">
<p class="font-body-md text-body-md">I'm having trouble connecting to the database server. It keeps timing out.</p>
</div>
<span class="text-[11px] font-label-sm text-on-surface-variant dark:text-secondary-fixed-dim mr-1">14:32</span>
</div>
<!-- Agent Message (Henry) -->
<div class="flex flex-col items-start gap-1">
<div class="flex items-end gap-2 max-w-[85%]">
<div class="w-6 h-6 rounded-full bg-primary-container flex items-center justify-center shrink-0 mb-1">
<span class="material-symbols-outlined text-[14px] text-on-primary-container">robot_2</span>
</div>
<div class="bg-surface-container-lowest dark:bg-on-secondary-fixed-variant border border-outline-variant/30 dark:border-transparent text-on-surface dark:text-surface-bright px-4 py-3 rounded-2xl rounded-tl-sm shadow-sm">
<p class="font-body-md text-body-md">I can help with that. Let's check the current status of the database cluster. Could you provide the cluster ID or the environment you are trying to access?</p>
</div>
</div>
<span class="text-[11px] font-label-sm text-on-surface-variant dark:text-secondary-fixed-dim ml-8">14:32</span>
</div>
<!-- User Message -->
<div class="flex flex-col items-end gap-1">
<div class="bg-primary dark:bg-primary-container text-on-primary dark:text-on-primary-container px-4 py-3 rounded-2xl rounded-tr-sm max-w-[85%] shadow-sm">
<p class="font-body-md text-body-md">It's the production cluster, ID: prod-db-main-01</p>
</div>
<span class="text-[11px] font-label-sm text-on-surface-variant dark:text-secondary-fixed-dim mr-1">14:34</span>
</div>
<!-- Agent Message (Henry) with Action/System Context -->
<div class="flex flex-col items-start gap-1">
<div class="flex items-end gap-2 max-w-[85%] w-full">
<div class="w-6 h-6 rounded-full bg-primary-container flex items-center justify-center shrink-0 mb-1">
<span class="material-symbols-outlined text-[14px] text-on-primary-container">robot_2</span>
</div>
<div class="bg-surface-container-lowest dark:bg-on-secondary-fixed-variant border border-outline-variant/30 dark:border-transparent text-on-surface dark:text-surface-bright p-4 rounded-2xl rounded-tl-sm shadow-sm w-full">
<p class="font-body-md text-body-md mb-3">Checking metrics for <code class="font-label-sm bg-surface-container dark:bg-on-secondary-fixed px-1 rounded text-primary dark:text-primary-fixed-dim">prod-db-main-01</code>...</p>
<!-- System Action Block (High-end UI pattern inside chat) -->
<div class="bg-surface-container dark:bg-inverse-surface/50 rounded-lg p-3 border border-outline-variant/20 font-label-sm text-label-sm text-on-surface-variant dark:text-secondary-fixed-dim">
<div class="flex justify-between items-center mb-2">
<span class="flex items-center gap-1 text-primary-fixed-dim"><span class="material-symbols-outlined text-[16px]">terminal</span> System Query</span>
<span>0.4s</span>
</div>
<div class="font-mono text-[11px] opacity-80 pl-2 border-l-2 border-outline-variant/30">
&gt; GET /api/v1/clusters/prod-db-main-01/metrics<br/>
&gt; Status: 200 OK<br/>
&gt; Connections: 1450/1500 (Warning)
</div>
</div>
<p class="font-body-md text-body-md mt-3">It looks like the cluster is nearing its maximum connection limit (1450 active out of 1500 max). This is likely causing the timeouts. Would you like me to temporarily increase the connection pool or investigate which service is holding the most connections?</p>
</div>
</div>
<span class="text-[11px] font-label-sm text-on-surface-variant dark:text-secondary-fixed-dim ml-8">14:35</span>
</div>
</div>
<!-- Panel Footer (Actions) -->
<div class="p-4 border-t border-outline-variant dark:border-on-secondary-container bg-surface-container-lowest dark:bg-inverse-surface shrink-0 flex gap-3">
<button class="flex-1 py-2 px-4 rounded-lg bg-surface-container dark:bg-on-secondary-fixed text-on-surface dark:text-surface-bright font-body-md text-body-md border border-outline-variant dark:border-transparent hover:bg-surface-container-high dark:hover:bg-on-secondary-fixed-variant transition-colors flex items-center justify-center gap-2">
<span class="material-symbols-outlined text-[18px]">download</span> Export Log
</button>
<button class="flex-1 py-2 px-4 rounded-lg bg-primary text-on-primary font-body-md text-body-md hover:bg-primary-container hover:text-on-primary-container transition-colors shadow-sm flex items-center justify-center gap-2">
Take Over <span class="material-symbols-outlined text-[18px]">front_hand</span>
</button>
</div>
</div>
<!-- Backdrop for Panel -->
<div class="fixed inset-0 bg-black/20 backdrop-blur-sm z-40 hidden opacity-0 transition-opacity duration-300" id="panelBackdrop" onclick="closeTranscript()"></div>
<script>
// Simple logic to toggle the slide-out panel
const panel = document.getElementById('transcriptPanel');
const backdrop = document.getElementById('panelBackdrop');
const panelUserId = document.getElementById('panelUserId');
const panelAgentName = document.getElementById('panelAgentName');
function toggleTranscript(userId, agentName) {
// Update panel header info conceptually
panelUserId.textContent = userId;
panelAgentName.textContent = agentName;
// Show backdrop
backdrop.classList.remove('hidden');
// Small delay to allow display block to render before opacity transition
setTimeout(() => {
backdrop.classList.remove('opacity-0');
}, 10);
// Slide in panel
panel.classList.remove('translate-x-full');
}
function closeTranscript() {
// Slide out panel
panel.classList.add('translate-x-full');
// Hide backdrop
backdrop.classList.add('opacity-0');
setTimeout(() => {
backdrop.classList.add('hidden');
}, 300); // match transition duration
}
</script>
</body></html>

322
Design/file_repository.html Normal file
View File

@ -0,0 +1,322 @@
<!DOCTYPE html>
<html class="dark" lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>Henry Admin - File Repository</title>
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com" rel="preconnect"/>
<link crossorigin="" href="https://fonts.gstatic.com" rel="preconnect"/>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&amp;family=JetBrains+Mono:wght@400;500&amp;display=swap" rel="stylesheet"/>
<!-- Material Symbols -->
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<!-- Theme Configuration -->
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"secondary": "#505f76",
"on-secondary-fixed-variant": "#38485d",
"on-primary-fixed-variant": "#004395",
"on-error": "#ffffff",
"surface-container": "#eceef0",
"surface-bright": "#f7f9fb",
"on-tertiary": "#ffffff",
"surface-dim": "#d8dadc",
"on-primary": "#ffffff",
"surface-container-highest": "#e0e3e5",
"tertiary-fixed": "#ffdcc6",
"tertiary-fixed-dim": "#ffb786",
"on-surface-variant": "#424754",
"on-primary-container": "#fefcff",
"primary-fixed": "#d8e2ff",
"surface-container-lowest": "#ffffff",
"inverse-on-surface": "#eff1f3",
"surface": "#f7f9fb",
"on-tertiary-container": "#fffbff",
"primary-container": "#2170e4",
"on-secondary-container": "#54647a",
"on-tertiary-fixed": "#311400",
"secondary-fixed-dim": "#b7c8e1",
"secondary-fixed": "#d3e4fe",
"primary-fixed-dim": "#adc6ff",
"outline-variant": "#c2c6d6",
"on-secondary": "#ffffff",
"inverse-surface": "#2d3133",
"on-primary-fixed": "#001a42",
"error-container": "#ffdad6",
"inverse-primary": "#adc6ff",
"tertiary-container": "#b75b00",
"background": "#f7f9fb",
"tertiary": "#924700",
"on-tertiary-fixed-variant": "#723600",
"on-secondary-fixed": "#0b1c30",
"on-error-container": "#93000a",
"surface-tint": "#005ac2",
"surface-container-low": "#f2f4f6",
"on-background": "#191c1e",
"outline": "#727785",
"surface-container-high": "#e6e8ea",
"surface-variant": "#e0e3e5",
"on-surface": "#191c1e",
"error": "#ba1a1a",
"secondary-container": "#d0e1fb",
"primary": "#0058be"
},
"borderRadius": {
"DEFAULT": "0.125rem",
"lg": "0.25rem",
"xl": "0.5rem",
"full": "0.75rem"
},
"spacing": {
"unit-2": "8px",
"base": "4px",
"unit-4": "16px",
"unit-6": "24px",
"container-max": "1440px",
"unit-1": "4px",
"sidebar-width": "260px",
"unit-12": "48px",
"unit-8": "32px"
},
"fontFamily": {
"label-sm": ["JetBrains Mono"],
"headline-md": ["Inter"],
"body-md": ["Inter"],
"headline-lg-mobile": ["Inter"],
"headline-lg": ["Inter"],
"body-lg": ["Inter"]
},
"fontSize": {
"label-sm": ["13px", {"lineHeight": "1", "letterSpacing": "0.05em", "fontWeight": "500"}],
"headline-md": ["24px", {"lineHeight": "1.3", "letterSpacing": "-0.01em", "fontWeight": "600"}],
"body-md": ["16px", {"lineHeight": "1.5", "fontWeight": "400"}],
"headline-lg-mobile": ["26px", {"lineHeight": "1.2", "fontWeight": "600"}],
"headline-lg": ["32px", {"lineHeight": "1.2", "letterSpacing": "-0.02em", "fontWeight": "600"}],
"body-lg": ["18px", {"lineHeight": "1.6", "fontWeight": "400"}]
}
}
}
}
</script>
<style>
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}
.icon-fill {
font-variation-settings: 'FILL' 1, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}
</style>
</head>
<body class="bg-background dark:bg-on-secondary-fixed text-on-surface dark:text-surface-bright flex min-h-screen">
<!-- SideNavBar -->
<nav class="hidden md:flex w-sidebar-width h-screen fixed left-0 top-0 flex-col py-unit-6 bg-surface-container-low dark:bg-on-secondary-fixed-variant border-r border-outline-variant dark:border-on-surface-variant z-50">
<div class="px-unit-6 mb-unit-8 flex items-center gap-unit-2">
<span aria-label="Henry AI Logo" class="material-symbols-outlined text-primary dark:text-primary-fixed-dim text-3xl icon-fill">psychiatry</span>
<div>
<h1 class="text-headline-md font-headline-md font-black text-primary dark:text-primary-fixed-dim">Henry Admin</h1>
<p class="font-label-sm text-label-sm text-on-surface-variant dark:text-surface-variant">v2.4.0-stable</p>
</div>
</div>
<ul class="flex flex-col flex-grow gap-unit-1 px-unit-4">
<!-- Agents (Inactive) -->
<li>
<a class="flex items-center gap-unit-4 px-unit-4 py-3 rounded-lg text-on-surface-variant dark:text-surface-variant hover:text-on-surface dark:hover:text-surface-bright hover:bg-surface-container-high dark:hover:bg-on-secondary-fixed transition-all duration-200 ease-in-out" href="#">
<span class="material-symbols-outlined">smart_toy</span>
<span class="font-body-md text-body-md">Agents</span>
</a>
</li>
<!-- History (Active Context) -->
<li>
<a class="flex items-center gap-unit-4 px-unit-4 py-3 rounded-lg bg-secondary-container dark:bg-on-secondary-fixed-variant text-primary dark:text-primary-fixed-dim border-l-4 border-primary dark:border-primary-fixed-dim font-bold transition-all duration-200 ease-in-out" href="#">
<span class="material-symbols-outlined">history</span>
<span class="font-body-md text-body-md">History</span>
</a>
</li>
<!-- Admins (Inactive) -->
<li>
<a class="flex items-center gap-unit-4 px-unit-4 py-3 rounded-lg text-on-surface-variant dark:text-surface-variant hover:text-on-surface dark:hover:text-surface-bright hover:bg-surface-container-high dark:hover:bg-on-secondary-fixed transition-all duration-200 ease-in-out" href="#">
<span class="material-symbols-outlined">admin_panel_settings</span>
<span class="font-body-md text-body-md">Admins</span>
</a>
</li>
<!-- Settings (Inactive) -->
<li>
<a class="flex items-center gap-unit-4 px-unit-4 py-3 rounded-lg text-on-surface-variant dark:text-surface-variant hover:text-on-surface dark:hover:text-surface-bright hover:bg-surface-container-high dark:hover:bg-on-secondary-fixed transition-all duration-200 ease-in-out" href="#">
<span class="material-symbols-outlined">settings</span>
<span class="font-body-md text-body-md">Settings</span>
</a>
</li>
<!-- Documentation (Inactive) -->
<li>
<a class="flex items-center gap-unit-4 px-unit-4 py-3 rounded-lg text-on-surface-variant dark:text-surface-variant hover:text-on-surface dark:hover:text-surface-bright hover:bg-surface-container-high dark:hover:bg-on-secondary-fixed transition-all duration-200 ease-in-out" href="#">
<span class="material-symbols-outlined">description</span>
<span class="font-body-md text-body-md">Documentation</span>
</a>
</li>
</ul>
<div class="mt-auto px-unit-4">
<ul class="flex flex-col gap-unit-1">
<li>
<a class="flex items-center gap-unit-4 px-unit-4 py-3 rounded-lg text-on-surface-variant dark:text-surface-variant hover:text-on-surface dark:hover:text-surface-bright hover:bg-surface-container-high dark:hover:bg-on-secondary-fixed transition-all duration-200 ease-in-out" href="#">
<span class="material-symbols-outlined">contact_support</span>
<span class="font-body-md text-body-md">Support</span>
</a>
</li>
<li>
<a class="flex items-center gap-unit-4 px-unit-4 py-3 rounded-lg text-on-surface-variant dark:text-surface-variant hover:text-on-surface dark:hover:text-surface-bright hover:bg-surface-container-high dark:hover:bg-on-secondary-fixed transition-all duration-200 ease-in-out" href="#">
<span class="material-symbols-outlined">logout</span>
<span class="font-body-md text-body-md">Log Out</span>
</a>
</li>
</ul>
</div>
</nav>
<!-- Main Content Area -->
<main class="flex-grow md:ml-sidebar-width flex flex-col min-h-screen">
<!-- TopNavBar -->
<header class="flex justify-between items-center px-unit-8 w-full sticky top-0 z-40 h-16 bg-surface-bright dark:bg-inverse-surface border-b border-outline-variant dark:border-on-surface-variant shadow-sm">
<div class="flex items-center gap-unit-4 w-1/3">
<span class="text-headline-md font-headline-md font-bold text-primary dark:text-primary-fixed-dim md:hidden">Henry</span>
<div class="relative w-full max-w-md hidden md:block">
<span class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant dark:text-surface-variant">search</span>
<input class="w-full bg-surface-container-low dark:bg-on-secondary-fixed-variant border border-outline-variant dark:border-on-surface-variant rounded-lg pl-10 pr-4 py-2 font-body-md text-body-md text-on-surface dark:text-surface-bright focus:outline-none focus:border-primary dark:focus:border-primary-fixed-dim transition-colors placeholder:text-on-surface-variant dark:placeholder:text-surface-variant/70" placeholder="Search files, agents, or contexts..." type="text"/>
</div>
</div>
<div class="flex items-center gap-unit-4">
<button class="p-2 rounded-full text-on-surface-variant dark:text-surface-variant hover:bg-surface-container dark:hover:bg-on-secondary-fixed-variant transition-colors cursor-pointer active:opacity-80">
<span class="material-symbols-outlined">notifications</span>
</button>
<button class="p-2 rounded-full text-on-surface-variant dark:text-surface-variant hover:bg-surface-container dark:hover:bg-on-secondary-fixed-variant transition-colors cursor-pointer active:opacity-80">
<span class="material-symbols-outlined">help_outline</span>
</button>
<div class="w-8 h-8 rounded-full bg-surface-container-high dark:bg-on-secondary-fixed-variant border border-outline-variant dark:border-on-surface-variant overflow-hidden ml-2 flex items-center justify-center">
<img alt="Administrator Profile" class="w-full h-full object-cover" data-alt="A minimalist, highly stylized circular profile avatar image for an administrator. The image features a subtle geometric abstraction of a human silhouette in deep blues and slate grays, set against a dark atmospheric background, conveying a sense of secure, high-end technical administration." src="https://lh3.googleusercontent.com/aida-public/AB6AXuDj49dhYimS1-63p4zHsiPyac80o--MFJvo_LWy32_bxmHoOk8yog866XTk8T8Xd-cAtK6jG3jyC58rquQhrOmp2TAkcXxT1TIv4MvLTkrLfHFyRxf5qfZ2qASfEsRbm6ldcQc-aMyBxXzQ4YOvXQa0ja6Kb7Q9Z3MdlCVUOhxIYnPbFbDGcKhnZuqDP_tQiUHHfMooXHL0DlhVfK4GI-175TGagp59rorZAk4oZxH-VIciX_f4idvn__YXHnHU4fJDW9MwW4umUcF0"/>
</div>
</div>
</header>
<!-- Canvas -->
<div class="p-unit-8 md:p-unit-12 flex-grow">
<!-- Header Section -->
<div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-unit-8 gap-unit-4">
<div>
<h2 class="font-headline-lg text-headline-lg text-on-surface dark:text-surface-bright mb-1">File Repository</h2>
<p class="font-body-md text-body-md text-on-surface-variant dark:text-surface-variant">Manage and access artifacts generated during Agent conversations.</p>
</div>
<div class="flex gap-unit-2 w-full md:w-auto">
<div class="relative flex-grow md:flex-grow-0">
<select class="appearance-none bg-surface-container-low dark:bg-inverse-surface border border-outline-variant dark:border-on-surface-variant rounded-lg px-4 py-2 pr-10 font-body-md text-body-md text-on-surface dark:text-surface-bright focus:outline-none focus:border-primary dark:focus:border-primary-fixed-dim cursor-pointer w-full">
<option>All Types</option>
<option>Documents (.pdf, .docx)</option>
<option>Data (.csv, .json)</option>
<option>Images (.png, .jpg)</option>
</select>
<span class="material-symbols-outlined absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none text-on-surface-variant dark:text-surface-variant">expand_more</span>
</div>
<button class="bg-surface-container-low dark:bg-inverse-surface border border-outline-variant dark:border-on-surface-variant rounded-lg px-4 py-2 font-body-md text-body-md flex items-center gap-2 hover:bg-surface-container-high dark:hover:bg-on-secondary-fixed-variant transition-colors">
<span class="material-symbols-outlined text-sm">filter_list</span>
<span>Filter</span>
</button>
</div>
</div>
<!-- Data Grid (Bento/Card-like List) -->
<div class="flex flex-col gap-unit-2">
<!-- Table Header -->
<div class="hidden md:grid grid-cols-12 gap-4 px-unit-6 py-3 border-b border-outline-variant dark:border-on-surface-variant text-on-surface-variant dark:text-surface-variant font-label-sm text-label-sm uppercase tracking-wider mb-2">
<div class="col-span-4">File Name</div>
<div class="col-span-2">Type</div>
<div class="col-span-2">Size</div>
<div class="col-span-2">Created Date</div>
<div class="col-span-2 text-right">Context</div>
</div>
<!-- Row 1 -->
<div class="grid grid-cols-1 md:grid-cols-12 gap-4 items-center px-unit-6 py-unit-4 bg-surface-bright dark:bg-inverse-surface border border-outline-variant dark:border-on-surface-variant rounded-lg hover:border-primary dark:hover:border-primary-fixed-dim/50 transition-colors group">
<div class="col-span-4 flex items-center gap-3">
<div class="w-10 h-10 rounded bg-primary-fixed/10 dark:bg-primary-fixed-dim/10 flex items-center justify-center text-primary dark:text-primary-fixed-dim">
<span class="material-symbols-outlined">table_chart</span>
</div>
<div>
<p class="font-body-md text-body-md font-medium text-on-surface dark:text-surface-bright truncate group-hover:text-primary dark:group-hover:text-primary-fixed-dim transition-colors">user_behavior_export_q3.csv</p>
<p class="font-label-sm text-label-sm text-on-surface-variant dark:text-surface-variant md:hidden mt-1">CSV Data • 2.4 MB</p>
</div>
</div>
<div class="col-span-2 hidden md:block">
<span class="inline-flex items-center px-2 py-1 rounded font-label-sm text-label-sm bg-surface-container dark:bg-on-secondary-fixed-variant text-on-surface-variant dark:text-surface-variant">CSV Data</span>
</div>
<div class="col-span-2 hidden md:block font-label-sm text-label-sm text-on-surface-variant dark:text-surface-variant">2.4 MB</div>
<div class="col-span-2 hidden md:block font-body-md text-body-md text-on-surface-variant dark:text-surface-variant">Oct 24, 14:32</div>
<div class="col-span-2 md:text-right">
<a class="inline-flex items-center gap-1 font-body-md text-body-md text-primary dark:text-primary-fixed-dim hover:underline" href="#">
<span class="material-symbols-outlined text-[18px]">chat</span>
<span>View Transcript</span>
</a>
</div>
</div>
<!-- Row 2 -->
<div class="grid grid-cols-1 md:grid-cols-12 gap-4 items-center px-unit-6 py-unit-4 bg-surface-bright dark:bg-inverse-surface border border-outline-variant dark:border-on-surface-variant rounded-lg hover:border-primary dark:hover:border-primary-fixed-dim/50 transition-colors group">
<div class="col-span-4 flex items-center gap-3">
<div class="w-10 h-10 rounded bg-tertiary-container/10 dark:bg-tertiary-fixed-dim/10 flex items-center justify-center text-tertiary dark:text-tertiary-fixed-dim">
<span class="material-symbols-outlined">code</span>
</div>
<div>
<p class="font-body-md text-body-md font-medium text-on-surface dark:text-surface-bright truncate group-hover:text-primary dark:group-hover:text-primary-fixed-dim transition-colors">auth_middleware_v2.ts</p>
<p class="font-label-sm text-label-sm text-on-surface-variant dark:text-surface-variant md:hidden mt-1">Source Code • 14 KB</p>
</div>
</div>
<div class="col-span-2 hidden md:block">
<span class="inline-flex items-center px-2 py-1 rounded font-label-sm text-label-sm bg-surface-container dark:bg-on-secondary-fixed-variant text-on-surface-variant dark:text-surface-variant">Source Code</span>
</div>
<div class="col-span-2 hidden md:block font-label-sm text-label-sm text-on-surface-variant dark:text-surface-variant">14 KB</div>
<div class="col-span-2 hidden md:block font-body-md text-body-md text-on-surface-variant dark:text-surface-variant">Oct 23, 09:15</div>
<div class="col-span-2 md:text-right">
<a class="inline-flex items-center gap-1 font-body-md text-body-md text-primary dark:text-primary-fixed-dim hover:underline" href="#">
<span class="material-symbols-outlined text-[18px]">chat</span>
<span>View Transcript</span>
</a>
</div>
</div>
<!-- Row 3 -->
<div class="grid grid-cols-1 md:grid-cols-12 gap-4 items-center px-unit-6 py-unit-4 bg-surface-bright dark:bg-inverse-surface border border-outline-variant dark:border-on-surface-variant rounded-lg hover:border-primary dark:hover:border-primary-fixed-dim/50 transition-colors group">
<div class="col-span-4 flex items-center gap-3">
<div class="w-10 h-10 rounded bg-error-container/20 flex items-center justify-center text-error dark:text-error-container">
<span class="material-symbols-outlined">picture_as_pdf</span>
</div>
<div>
<p class="font-body-md text-body-md font-medium text-on-surface dark:text-surface-bright truncate group-hover:text-primary dark:group-hover:text-primary-fixed-dim transition-colors">infrastructure_diagram_final.pdf</p>
<p class="font-label-sm text-label-sm text-on-surface-variant dark:text-surface-variant md:hidden mt-1">Document • 1.2 MB</p>
</div>
</div>
<div class="col-span-2 hidden md:block">
<span class="inline-flex items-center px-2 py-1 rounded font-label-sm text-label-sm bg-surface-container dark:bg-on-secondary-fixed-variant text-on-surface-variant dark:text-surface-variant">Document</span>
</div>
<div class="col-span-2 hidden md:block font-label-sm text-label-sm text-on-surface-variant dark:text-surface-variant">1.2 MB</div>
<div class="col-span-2 hidden md:block font-body-md text-body-md text-on-surface-variant dark:text-surface-variant">Oct 21, 16:00</div>
<div class="col-span-2 md:text-right">
<a class="inline-flex items-center gap-1 font-body-md text-body-md text-primary dark:text-primary-fixed-dim hover:underline" href="#">
<span class="material-symbols-outlined text-[18px]">chat</span>
<span>View Transcript</span>
</a>
</div>
</div>
</div>
<!-- Pagination (Simple) -->
<div class="flex justify-between items-center mt-unit-6 border-t border-outline-variant dark:border-on-surface-variant pt-unit-4">
<span class="font-label-sm text-label-sm text-on-surface-variant dark:text-surface-variant">Showing 1-3 of 42 files</span>
<div class="flex gap-2">
<button class="p-2 border border-outline-variant dark:border-on-surface-variant rounded hover:bg-surface-container dark:hover:bg-on-secondary-fixed-variant transition-colors disabled:opacity-50" disabled="">
<span class="material-symbols-outlined">chevron_left</span>
</button>
<button class="p-2 border border-outline-variant dark:border-on-surface-variant rounded hover:bg-surface-container dark:hover:bg-on-secondary-fixed-variant transition-colors">
<span class="material-symbols-outlined">chevron_right</span>
</button>
</div>
</div>
</div>
</main>
</body></html>

140
Design/login.html Normal file
View File

@ -0,0 +1,140 @@
<!DOCTYPE html><html class="dark" lang="en"><head>
<meta charset="utf-8">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<title>AgentOS Login</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&amp;family=JetBrains+Mono:wght@500&amp;display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet">
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"on-tertiary-container": "#fffbff",
"on-primary": "#ffffff",
"surface": "#f7f9fb",
"inverse-surface": "#2d3133",
"primary-fixed-dim": "#adc6ff",
"primary-fixed": "#d8e2ff",
"primary-container": "#2170e4",
"surface-dim": "#d8dadc",
"on-surface": "#191c1e",
"tertiary-container": "#b75b00",
"secondary-fixed-dim": "#b7c8e1",
"tertiary-fixed-dim": "#ffb786",
"on-surface-variant": "#424754",
"on-background": "#191c1e",
"tertiary": "#924700",
"secondary": "#505f76",
"tertiary-fixed": "#ffdcc6",
"on-secondary": "#ffffff",
"on-error": "#ffffff",
"on-secondary-fixed": "#0b1c30",
"background": "#f7f9fb",
"outline-variant": "#c2c6d6",
"on-tertiary-fixed-variant": "#723600",
"surface-container-highest": "#e0e3e5",
"surface-container-lowest": "#ffffff",
"outline": "#727785",
"surface-tint": "#005ac2",
"error": "#ba1a1a",
"inverse-on-surface": "#eff1f3",
"secondary-fixed": "#d3e4fe",
"on-tertiary": "#ffffff",
"surface-bright": "#f7f9fb",
"on-error-container": "#93000a",
"secondary-container": "#d0e1fb",
"on-secondary-container": "#54647a",
"surface-container-low": "#f2f4f6",
"on-primary-fixed-variant": "#004395",
"surface-container-high": "#e6e8ea",
"error-container": "#ffdad6",
"on-secondary-fixed-variant": "#38485d",
"on-primary-container": "#fefcff",
"primary": "#0058be",
"surface-container": "#eceef0",
"surface-variant": "#e0e3e5",
"on-tertiary-fixed": "#311400",
"inverse-primary": "#adc6ff",
"on-primary-fixed": "#001a42"
},
"borderRadius": {
"DEFAULT": "0.125rem",
"lg": "0.25rem",
"xl": "0.5rem",
"full": "0.75rem"
},
"spacing": {
"unit-2": "8px",
"base": "4px",
"unit-4": "16px",
"unit-6": "24px",
"unit-12": "48px",
"container-max": "1440px",
"unit-8": "32px",
"unit-1": "4px",
"sidebar-width": "260px"
},
"fontFamily": {
"headline-lg": ["Inter"],
"label-sm": ["JetBrains Mono"],
"headline-md": ["Inter"],
"body-lg": ["Inter"],
"headline-lg-mobile": ["Inter"],
"body-md": ["Inter"]
},
"fontSize": {
"headline-lg": ["32px", { "lineHeight": "1.2", "letterSpacing": "-0.02em", "fontWeight": "600" }],
"label-sm": ["13px", { "lineHeight": "1", "letterSpacing": "0.05em", "fontWeight": "500" }],
"headline-md": ["24px", { "lineHeight": "1.3", "letterSpacing": "-0.01em", "fontWeight": "600" }],
"body-lg": ["18px", { "lineHeight": "1.6", "fontWeight": "400" }],
"headline-lg-mobile": ["26px", { "lineHeight": "1.2", "fontWeight": "600" }],
"body-md": ["16px", { "lineHeight": "1.5", "fontWeight": "400" }]
}
}
}
}
</script>
<style>
body {
background-color: #f7f9fb; /* light mode fallback */
}
.dark body {
background-color: #191c1e; /* dark mode background equivalent */
}
</style>
</head>
<body class="antialiased text-on-surface dark:text-inverse-on-surface min-h-screen flex items-center justify-center p-unit-4 md:p-unit-8 transition-colors duration-300">
<main class="w-full max-w-md rounded-xl overflow-hidden relative">
<div class="p-unit-6 md:p-unit-8 flex flex-col gap-unit-6">
<div class="text-center flex flex-col items-center gap-unit-2">
<span class="material-symbols-outlined text-[48px] text-primary dark:text-primary-fixed-dim" style="font-variation-settings: 'FILL' 1;">smart_toy</span>
<h1 class="font-headline-lg text-headline-lg md:font-headline-lg md:text-headline-lg text-on-surface dark:text-inverse-on-surface">Henry</h1>
<p class="font-body-md text-body-md text-on-surface-variant dark:text-surface-variant">System Control Access</p>
</div>
<form class="flex flex-col gap-unit-4" onsubmit="event.preventDefault();">
<div class="flex flex-col gap-unit-2">
<label class="font-label-sm text-label-sm text-on-surface-variant dark:text-surface-variant uppercase" for="email">Email Address</label>
<input class="w-full bg-surface-container-lowest dark:bg-inverse-on-surface/10 border border-outline-variant dark:border-outline rounded focus:border-primary dark:focus:border-primary-fixed-dim focus:ring-1 focus:ring-primary dark:focus:ring-primary-fixed-dim outline-none px-unit-4 py-unit-2 font-body-md text-body-md text-on-surface dark:text-inverse-on-surface transition-colors" id="email" placeholder="admin@agent.os" required="" type="email">
</div>
<div class="flex flex-col gap-unit-2">
<div class="flex justify-between items-center">
<label class="font-label-sm text-label-sm text-on-surface-variant dark:text-surface-variant uppercase" for="password">Password</label>
<a class="font-label-sm text-label-sm text-primary dark:text-primary-fixed-dim hover:underline opacity-80 hover:opacity-100 transition-opacity" href="#">Forgot?</a>
</div>
<input class="w-full bg-surface-container-lowest dark:bg-inverse-on-surface/10 border border-outline-variant dark:border-outline rounded focus:border-primary dark:focus:border-primary-fixed-dim focus:ring-1 focus:ring-primary dark:focus:ring-primary-fixed-dim outline-none px-unit-4 py-unit-2 font-body-md text-body-md text-on-surface dark:text-inverse-on-surface transition-colors" id="password" placeholder="••••••••" required="" type="password">
</div>
<div class="mt-unit-2">
<button class="w-full bg-primary hover:bg-primary/90 text-on-primary font-body-md text-body-md font-semibold py-3 rounded-lg flex items-center justify-center gap-unit-2 transition-colors focus:ring-2 focus:ring-offset-2 focus:ring-primary dark:focus:ring-offset-inverse-surface" type="submit">
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 1;">login</span>
Sign In
</button>
</div>
</form>
</div>
</main>
</body></html>

View File

@ -0,0 +1,179 @@
<!DOCTYPE html>
<html class="dark" lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>Henry - Password Recovery</title>
<!-- Fonts -->
<link href="https://fonts.googleapis.com" rel="preconnect"/>
<link crossorigin="" href="https://fonts.gstatic.com" rel="preconnect"/>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&amp;family=JetBrains+Mono:wght@500&amp;display=swap" rel="stylesheet"/>
<!-- Material Symbols -->
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<style>
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}
/* Subtle atmospheric glow for the floating effect */
.ambient-glow {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 40vw;
height: 40vw;
background: radial-gradient(circle, rgba(173, 198, 255, 0.03) 0%, rgba(25, 28, 30, 0) 70%);
pointer-events: none;
z-index: 0;
}
</style>
<!-- Tailwind Config -->
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"secondary": "#505f76",
"on-surface": "#191c1e",
"on-background": "#191c1e",
"primary-fixed": "#d8e2ff",
"outline-variant": "#c2c6d6",
"inverse-on-surface": "#eff1f3",
"surface": "#f7f9fb",
"on-surface-variant": "#424754",
"inverse-surface": "#2d3133",
"on-tertiary": "#ffffff",
"surface-bright": "#f7f9fb",
"tertiary": "#924700",
"on-tertiary-fixed": "#311400",
"surface-tint": "#005ac2",
"error-container": "#ffdad6",
"background": "#f7f9fb",
"on-primary": "#ffffff",
"on-primary-fixed": "#001a42",
"tertiary-fixed-dim": "#ffb786",
"surface-container": "#eceef0",
"error": "#ba1a1a",
"on-error-container": "#93000a",
"tertiary-fixed": "#ffdcc6",
"surface-container-highest": "#e0e3e5",
"on-secondary": "#ffffff",
"secondary-fixed-dim": "#b7c8e1",
"surface-variant": "#e0e3e5",
"secondary-container": "#d0e1fb",
"on-secondary-container": "#54647a",
"surface-container-lowest": "#ffffff",
"on-tertiary-container": "#fffbff",
"on-primary-fixed-variant": "#004395",
"primary-fixed-dim": "#adc6ff",
"surface-dim": "#d8dadc",
"secondary-fixed": "#d3e4fe",
"on-tertiary-fixed-variant": "#723600",
"tertiary-container": "#b75b00",
"primary-container": "#2170e4",
"on-secondary-fixed-variant": "#38485d",
"surface-container-high": "#e6e8ea",
"outline": "#727785",
"on-error": "#ffffff",
"surface-container-low": "#f2f4f6",
"primary": "#0058be",
"on-primary-container": "#fefcff",
"inverse-primary": "#adc6ff",
"on-secondary-fixed": "#0b1c30"
},
"borderRadius": {
"DEFAULT": "0.125rem",
"lg": "0.25rem",
"xl": "0.5rem",
"full": "0.75rem"
},
"spacing": {
"unit-1": "4px",
"unit-4": "16px",
"unit-12": "48px",
"unit-6": "24px",
"base": "4px",
"sidebar-width": "260px",
"container-max": "1440px",
"unit-8": "32px",
"unit-2": "8px"
},
"fontFamily": {
"body-lg": ["Inter"],
"headline-lg": ["Inter"],
"body-md": ["Inter"],
"label-sm": ["JetBrains Mono"],
"headline-md": ["Inter"],
"headline-lg-mobile": ["Inter"]
},
"fontSize": {
"body-lg": ["18px", { "lineHeight": "1.6", "fontWeight": "400" }],
"headline-lg": ["32px", { "lineHeight": "1.2", "letterSpacing": "-0.02em", "fontWeight": "600" }],
"body-md": ["16px", { "lineHeight": "1.5", "fontWeight": "400" }],
"label-sm": ["13px", { "lineHeight": "1", "letterSpacing": "0.05em", "fontWeight": "500" }],
"headline-md": ["24px", { "lineHeight": "1.3", "letterSpacing": "-0.01em", "fontWeight": "600" }],
"headline-lg-mobile": ["26px", { "lineHeight": "1.2", "fontWeight": "600" }]
}
}
}
}
</script>
</head>
<body class="bg-surface dark:bg-on-background text-on-surface dark:text-inverse-on-surface min-h-screen flex items-center justify-center relative overflow-hidden font-body-md antialiased selection:bg-primary-fixed-dim selection:text-on-primary-fixed">
<!-- Atmospheric Glow Background -->
<div class="ambient-glow"></div>
<!-- Main Content Canvas (Floating, no visible container bounds) -->
<main class="w-full max-w-sm px-unit-6 relative z-10 flex flex-col">
<!-- Brand / Header Section -->
<header class="flex flex-col items-center mb-unit-12 text-center">
<div class="flex items-center gap-unit-2 mb-unit-2 text-primary dark:text-primary-fixed-dim">
<span class="material-symbols-outlined text-[32px]" data-icon="vpn_key" data-weight="fill" style="font-variation-settings: 'FILL' 1;">vpn_key</span>
</div>
<h1 class="font-headline-lg text-headline-lg text-on-surface dark:text-inverse-on-surface tracking-tight">Henry</h1>
<p class="font-label-sm text-label-sm text-on-surface-variant dark:text-surface-variant uppercase tracking-[0.15em] mt-unit-2 opacity-80">System Control Access</p>
</header>
<!-- Recovery Form Area -->
<form class="flex flex-col gap-unit-6" onsubmit="event.preventDefault();">
<div class="flex flex-col gap-unit-2">
<p class="font-body-md text-body-md text-on-surface-variant dark:text-surface-variant text-center mb-unit-2">
Enter your registered email to receive reset instructions.
</p>
</div>
<!-- Input Group -->
<div class="relative flex flex-col group">
<label class="font-label-sm text-label-sm text-on-surface-variant dark:text-surface-variant mb-unit-2 transition-colors group-focus-within:text-primary dark:group-focus-within:text-primary-fixed-dim" for="email">Email Address</label>
<div class="relative flex items-center">
<span class="material-symbols-outlined absolute left-unit-4 text-outline dark:text-outline text-[20px] pointer-events-none" data-icon="mail">mail</span>
<input class="w-full bg-transparent border border-outline-variant dark:border-outline rounded-lg py-unit-4 pl-[44px] pr-unit-4 text-on-surface dark:text-inverse-on-surface font-label-sm text-label-sm placeholder-outline dark:placeholder-outline/50 focus:border-primary dark:focus:border-primary-fixed-dim focus:ring-1 focus:ring-primary dark:focus:ring-primary-fixed-dim outline-none transition-all" id="email" name="email" placeholder="operator@henry.sys" required="" type="email"/>
</div>
</div>
<!-- Action Button -->
<button class="w-full bg-primary text-on-primary py-unit-4 px-unit-6 rounded-lg font-label-sm text-label-sm uppercase tracking-wide hover:bg-on-primary-fixed-variant transition-colors flex items-center justify-center gap-unit-2 mt-unit-2 focus:outline-none focus:ring-2 focus:ring-primary-fixed focus:ring-offset-2 focus:ring-offset-on-background" type="submit">
Send Reset Link
<span class="material-symbols-outlined text-[18px]" data-icon="arrow_forward">arrow_forward</span>
</button>
</form>
<!-- Navigation Link -->
<div class="mt-unit-12 text-center">
<a class="inline-flex items-center gap-unit-2 text-primary dark:text-primary-fixed-dim hover:text-primary-container dark:hover:text-primary-fixed font-label-sm text-label-sm transition-colors opacity-90 hover:opacity-100" href="#">
<span class="material-symbols-outlined text-[16px]" data-icon="arrow_back">arrow_back</span>
Back to Login
</a>
</div>
</main>
<!-- Inject Material Symbols logic -->
<script>
document.querySelectorAll('[data-icon]').forEach(el => {
const iconName = el.getAttribute('data-icon');
el.textContent = iconName;
el.classList.add('material-symbols-outlined');
if (el.getAttribute('data-weight') === 'fill') {
el.style.fontVariationSettings = "'FILL' 1";
}
});
</script>
</body></html>

View File

@ -38,6 +38,8 @@ HENRY_FILE_PATH=~/.henry/files # Path where the server stores created files and
``` ```
Start the server: `dotnet run --project Source/Henry.Server` Start the server: `dotnet run --project Source/Henry.Server`
> **Note:** Upon the first launch, the server automatically initializes the database and creates a default administrator account. You can log into the Admin Dashboard using the username **`admin`** and password **`admin`**.
### 2. Usage Example (Henry.Client) ### 2. Usage Example (Henry.Client)
```csharp ```csharp
@ -69,7 +71,19 @@ if (client.Files.Any())
} }
``` ```
### 3. Creating Tools ### 3. Uploading User Files
Consumers can supply contextual documents to the LLM. The `HenryClient` uploads files seamlessly, injecting the file contents straight into the conversation memory:
```csharp
// Upload a document. If ConversationId is null, a new conversation is automatically started.
var uploadedGuid = await client.UploadFileAsync("/path/to/Market_Research.pdf");
// The LLM can now read the shadow-parsed markdown context of your file!
client.AddUserMessage("Please review the file I just uploaded.");
var response = await client.GenerateResponseAsync();
```
### 4. Creating Tools
To expose a local function to the LLM, define a static class and use the `[HenryTool]` attribute. Use `[HenrySystemParameter]` for parameters that should be auto-injected by the system instead of requested from the LLM (like `Guid conversationId`): To expose a local function to the LLM, define a static class and use the `[HenryTool]` attribute. Use `[HenrySystemParameter]` for parameters that should be auto-injected by the system instead of requested from the LLM (like `Guid conversationId`):
```csharp ```csharp
@ -85,7 +99,7 @@ public static class TimeTools
} }
``` ```
### 4. File Management & Web API ### 5. File Management & Web API
Henry Server ships with a built-in document engine. Server-side tools can generate PDFs, Word documents, and text files. You can retrieve files dynamically via the API: Henry Server ships with a built-in document engine. Server-side tools can generate PDFs, Word documents, and text files. You can retrieve files dynamically via the API:
- **Get File Content:** `GET /api/v1/files/{conversationId}/{fileGuid}` (Downloads the latest version) - **Get File Content:** `GET /api/v1/files/{conversationId}/{fileGuid}` (Downloads the latest version)

View File

@ -92,12 +92,108 @@ else
client.StartConversation(); client.StartConversation();
} }
var chatHistory = new List<string>();
while (true) while (true)
{ {
Console.Write("You: "); Console.Write("You: ");
var input = Console.ReadLine(); var input = ReadLineAutoSubmit(chatHistory);
if (string.IsNullOrWhiteSpace(input)) continue; if (string.IsNullOrWhiteSpace(input)) continue;
if (input.Trim().Equals("exit", StringComparison.OrdinalIgnoreCase)) break; if (input.Trim().Equals("exit", StringComparison.OrdinalIgnoreCase)) break;
chatHistory.Add(input);
// Auto-detect drag-and-drop or clipboard pasted files
// Terminal emulators paste files as paths (often quoted and space-separated)
var matches = System.Text.RegularExpressions.Regex.Matches(input, @"[""']([^""']+)[""']|([^\s""']+)");
var parsedPaths = matches.Select(m => m.Groups[1].Success ? m.Groups[1].Value : m.Groups[2].Value).ToList();
bool allValidFiles = parsedPaths.Any() && parsedPaths.All(p => {
try { return System.IO.File.Exists(p); } catch { return false; }
});
if (allValidFiles)
{
foreach (var filePath in parsedPaths)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"Uploading {filePath}...");
Console.ResetColor();
try
{
var fileGuid = await client.UploadFileAsync(filePath);
if (!string.IsNullOrEmpty(fileGuid))
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("File uploaded successfully.");
Console.ResetColor();
// Add a user message to inform the agent that a file was uploaded
client.AddUserMessage($"[System: The user has uploaded a file named '{System.IO.Path.GetFileName(filePath)}' with GUID: {fileGuid}]", isInternal: true);
}
else
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("Failed to upload file.");
Console.ResetColor();
}
}
catch (Exception ex)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"Error uploading file: {ex.Message}");
Console.ResetColor();
}
}
continue;
}
if (input.Trim().StartsWith("/upload ", StringComparison.OrdinalIgnoreCase))
{
var filePath = input.Trim().Substring(8).Trim();
if (!System.IO.File.Exists(filePath))
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"Error: File not found: {filePath}");
Console.ResetColor();
continue;
}
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"Uploading {filePath}...");
Console.ResetColor();
try
{
var fileGuid = await client.UploadFileAsync(filePath);
if (!string.IsNullOrEmpty(fileGuid))
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("File uploaded successfully.");
Console.ResetColor();
// Add a user message to inform the agent that a file was uploaded
client.AddUserMessage($"[System: The user has uploaded a file named '{System.IO.Path.GetFileName(filePath)}' with GUID: {fileGuid}]", isInternal: true);
}
else
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("Failed to upload file.");
Console.ResetColor();
}
}
catch (Exception ex)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"Error uploading file: {ex.Message}");
Console.ResetColor();
}
continue;
}
client.AddUserMessage(input); client.AddUserMessage(input);
@ -128,3 +224,111 @@ if (client.ConversationId.HasValue)
Console.WriteLine($"\nTo resume this conversation later, use:"); Console.WriteLine($"\nTo resume this conversation later, use:");
Console.WriteLine($"dotnet run --resume {client.ConversationId.Value}"); Console.WriteLine($"dotnet run --resume {client.ConversationId.Value}");
} }
static string ReadLineAutoSubmit(List<string> history)
{
var sb = new System.Text.StringBuilder();
int pos = 0;
int historyIndex = history.Count;
while (true)
{
var keyInfo = Console.ReadKey(intercept: true);
if (keyInfo.Modifiers == ConsoleModifiers.Control && keyInfo.Key == ConsoleKey.C)
{
Environment.Exit(0);
}
if (keyInfo.Key == ConsoleKey.Enter)
{
Console.WriteLine();
break;
}
else if (keyInfo.Key == ConsoleKey.Backspace && pos > 0)
{
sb.Remove(pos - 1, 1);
pos--;
Console.Write("\b" + sb.ToString(pos, sb.Length - pos) + " \b");
for (int i = 0; i < sb.Length - pos; i++) Console.Write("\b");
}
else if (keyInfo.Key == ConsoleKey.Delete && pos < sb.Length)
{
sb.Remove(pos, 1);
Console.Write(sb.ToString(pos, sb.Length - pos) + " \b");
for (int i = 0; i < sb.Length - pos; i++) Console.Write("\b");
}
else if (keyInfo.Key == ConsoleKey.LeftArrow && pos > 0)
{
pos--;
Console.Write("\b");
}
else if (keyInfo.Key == ConsoleKey.RightArrow && pos < sb.Length)
{
Console.Write(sb[pos]);
pos++;
}
else if (keyInfo.Key == ConsoleKey.Home)
{
while (pos > 0) { Console.Write("\b"); pos--; }
}
else if (keyInfo.Key == ConsoleKey.End)
{
while (pos < sb.Length) { Console.Write(sb[pos]); pos++; }
}
else if (keyInfo.Key == ConsoleKey.UpArrow)
{
if (historyIndex > 0)
{
historyIndex--;
while (pos > 0) { Console.Write("\b \b"); pos--; }
for (int i = 0; i < sb.Length; i++) Console.Write(" \b");
sb.Clear();
sb.Append(history[historyIndex]);
Console.Write(sb.ToString());
pos = sb.Length;
}
}
else if (keyInfo.Key == ConsoleKey.DownArrow)
{
if (historyIndex < history.Count)
{
historyIndex++;
while (pos > 0) { Console.Write("\b \b"); pos--; }
for (int i = 0; i < sb.Length; i++) Console.Write(" \b");
sb.Clear();
if (historyIndex < history.Count)
{
sb.Append(history[historyIndex]);
}
Console.Write(sb.ToString());
pos = sb.Length;
}
}
else if (!char.IsControl(keyInfo.KeyChar))
{
sb.Insert(pos, keyInfo.KeyChar);
Console.Write(sb.ToString(pos, sb.Length - pos));
pos++;
for (int i = 0; i < sb.Length - pos; i++) Console.Write("\b");
}
if (!Console.KeyAvailable && sb.Length > 0)
{
var currentInput = sb.ToString();
var matches = System.Text.RegularExpressions.Regex.Matches(currentInput, @"[""']([^""']+)[""']|([^\s""']+)");
var parsedPaths = matches.Select(m => m.Groups[1].Success ? m.Groups[1].Value : m.Groups[2].Value).ToList();
bool allValidFiles = parsedPaths.Any() && parsedPaths.All(p => {
try { return System.IO.File.Exists(p); } catch { return false; }
});
if (allValidFiles)
{
Console.WriteLine();
break;
}
}
}
return sb.ToString();
}

View File

@ -130,7 +130,6 @@ CRITICAL: Do not wrap the JSON in markdown blocks (```json). Do not add ANY conv
if (chatResponse == null || chatResponse.Message == null) return null; if (chatResponse == null || chatResponse.Message == null) return null;
this.ConversationId = chatResponse.ConversationId; this.ConversationId = chatResponse.ConversationId;
var agentMessage = chatResponse.Message;
try try
{ {
@ -143,6 +142,8 @@ CRITICAL: Do not wrap the JSON in markdown blocks (```json). Do not add ANY conv
} }
catch { } catch { }
var agentMessage = chatResponse.Message;
string content = agentMessage.Content.Trim(); string content = agentMessage.Content.Trim();
if (!includeReasoning) if (!includeReasoning)
@ -472,4 +473,47 @@ CRITICAL: Do not wrap the JSON in markdown blocks (```json). Do not add ANY conv
} }
return sb.ToString(); return sb.ToString();
} }
public async Task<string?> UploadFileAsync(string filePath)
{
using var content = new MultipartFormDataContent();
var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
var streamContent = new StreamContent(fileStream);
var fileName = Path.GetFileName(filePath);
content.Add(streamContent, "file", fileName);
if (ConversationId.HasValue)
{
content.Add(new StringContent(ConversationId.Value.ToString()), "conversationId");
}
var request = new HttpRequestMessage(HttpMethod.Post, $"/api/v1/files/upload");
request.Content = content;
if (!string.IsNullOrEmpty(ApiKey))
{
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", ApiKey);
}
var response = await _httpClient.SendAsync(request);
if (response.IsSuccessStatusCode)
{
var uploadResponse = await response.Content.ReadFromJsonAsync<JsonElement>();
if (uploadResponse.TryGetProperty("conversationId", out var cidElement) || uploadResponse.TryGetProperty("ConversationId", out cidElement))
{
if (Guid.TryParse(cidElement.GetString(), out var cid))
{
ConversationId = cid;
}
}
string? fileGuid = null;
if (uploadResponse.TryGetProperty("fileGuid", out var fgElement) || uploadResponse.TryGetProperty("FileGuid", out fgElement))
{
fileGuid = fgElement.GetString();
}
return fileGuid;
}
return null;
}
} }

View File

@ -13,6 +13,8 @@ using Henry.Server.Storage;
using Henry.Server.Tools; using Henry.Server.Tools;
using Henry.Client; using Henry.Client;
using System.Text.Json; using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
namespace Henry.Server.Endpoints; namespace Henry.Server.Endpoints;
@ -42,6 +44,10 @@ public static class ApiV1Endpoints
group.MapGet("/files/recent/{conversationId:guid}", GetRecentFiles) group.MapGet("/files/recent/{conversationId:guid}", GetRecentFiles)
.WithName("GetRecentFiles"); .WithName("GetRecentFiles");
group.MapPost("/files/upload", UploadFile)
.WithName("UploadFile")
.DisableAntiforgery();
} }
private static IResult GetRecentFiles(Guid conversationId, DateTime since, AgentDbContext dbContext) private static IResult GetRecentFiles(Guid conversationId, DateTime since, AgentDbContext dbContext)
@ -57,6 +63,101 @@ public static class ApiV1Endpoints
return Results.Ok(newFiles); return Results.Ok(newFiles);
} }
private static async Task<IResult> UploadFile(HttpContext context, AgentDbContext dbContext)
{
if (!context.Request.HasFormContentType)
return Results.BadRequest("Unsupported content type");
var form = await context.Request.ReadFormAsync();
var file = form.Files.FirstOrDefault();
if (file == null || file.Length == 0)
return Results.BadRequest("No file provided");
Guid conversationId;
if (form.TryGetValue("conversationId", out var cidValue) && Guid.TryParse(cidValue, out var parsedCid))
{
conversationId = parsedCid;
var exists = dbContext.Conversations.Any(c => c.Id == conversationId);
if (!exists)
{
dbContext.Conversations.Add(new ConversationEntity { Id = conversationId, Title = "New Conversation" });
await dbContext.SaveChangesAsync();
}
}
else
{
conversationId = Guid.NewGuid();
dbContext.Conversations.Add(new ConversationEntity { Id = conversationId, Title = "New Conversation" });
await dbContext.SaveChangesAsync();
}
var fileGuid = Guid.NewGuid();
var extension = System.IO.Path.GetExtension(file.FileName);
var basePath = Henry.Server.Tools.FileTools.GetBasePath();
var physicalName = $"{fileGuid}{extension}";
var fullPath = System.IO.Path.Combine(basePath, physicalName);
using (var stream = new System.IO.FileStream(fullPath, System.IO.FileMode.Create))
{
await file.CopyToAsync(stream);
}
var provider = new Microsoft.AspNetCore.StaticFiles.FileExtensionContentTypeProvider();
string mimeType = provider.TryGetContentType(file.FileName, out string? contentType) ? contentType : "application/octet-stream";
var fileRecord1 = new FileEntity
{
ConversationId = conversationId,
FileGuid = fileGuid,
FileName = file.FileName,
MimeType = mimeType,
Version = 1,
CreatedAt = DateTime.UtcNow
};
dbContext.Files.Add(fileRecord1);
await dbContext.SaveChangesAsync();
if (mimeType == "application/pdf" || mimeType == "application/vnd.openxmlformats-officedocument.wordprocessingml.document" || mimeType == "application/msword")
{
try
{
var doc = new Aspose.Words.Document(fullPath);
// Aggressively clear all headers and footers to prevent pagination/shape artifacts
foreach (Aspose.Words.Section section in doc.Sections)
{
section.HeadersFooters.Clear();
}
// Remove all shapes and group shapes (images, diagrams, metafiles)
// to prevent Aspose from invoking SkiaSharp rendering which crashes on some vector formats.
doc.GetChildNodes(Aspose.Words.NodeType.Shape, true).Clear();
doc.GetChildNodes(Aspose.Words.NodeType.GroupShape, true).Clear();
var ms = new System.IO.MemoryStream();
var saveOptions = new Aspose.Words.Saving.MarkdownSaveOptions
{
ExportImagesAsBase64 = false,
ImageSavingCallback = new SkipImageSavingCallback()
};
doc.Save(ms, saveOptions);
ms.Position = 0;
var markdownText = await new System.IO.StreamReader(ms).ReadToEndAsync();
var mdPath = System.IO.Path.Combine(basePath, $"{fileGuid}.version1");
await System.IO.File.WriteAllTextAsync(mdPath, markdownText);
}
catch (Exception ex)
{
Console.WriteLine($"Aspose parsing failed: {ex}");
}
}
return Results.Ok(new { FileGuid = fileGuid, FileName = file.FileName, ConversationId = conversationId });
}
private static async Task<IResult> GetFileContent(Guid conversationId, Guid fileGuid, AgentDbContext dbContext, int? version = null) private static async Task<IResult> GetFileContent(Guid conversationId, Guid fileGuid, AgentDbContext dbContext, int? version = null)
{ {
var query = dbContext.Files.Where(f => f.ConversationId == conversationId && f.FileGuid == fileGuid); var query = dbContext.Files.Where(f => f.ConversationId == conversationId && f.FileGuid == fileGuid);
@ -71,12 +172,23 @@ public static class ApiV1Endpoints
var basePath = Henry.Server.Tools.FileTools.GetBasePath(); var basePath = Henry.Server.Tools.FileTools.GetBasePath();
var fullPath = System.IO.Path.Combine(basePath, $"{fileRecord.FileGuid}.version{fileRecord.Version}"); var fullPath = System.IO.Path.Combine(basePath, $"{fileRecord.FileGuid}.version{fileRecord.Version}");
var ext = System.IO.Path.GetExtension(fileRecord.FileName);
var originalPath = System.IO.Path.Combine(basePath, $"{fileRecord.FileGuid}{ext}");
if (fileRecord.Version == 1 && System.IO.File.Exists(originalPath))
{
fullPath = originalPath;
}
else if (!System.IO.File.Exists(fullPath))
{
fullPath = originalPath;
}
if (!System.IO.File.Exists(fullPath)) return Results.NotFound(new { Message = "Physical file not found" }); if (!System.IO.File.Exists(fullPath)) return Results.NotFound(new { Message = "Physical file not found" });
var mimeType = fileRecord.MimeType; var mimeType = fileRecord.MimeType;
if (mimeType == "application/pdf" || mimeType == "application/vnd.openxmlformats-officedocument.wordprocessingml.document") if (fullPath.Contains(".version") && (mimeType == "application/pdf" || mimeType == "application/vnd.openxmlformats-officedocument.wordprocessingml.document" || mimeType == "application/msword"))
{ {
var markdownContent = await System.IO.File.ReadAllTextAsync(fullPath); var markdownContent = await System.IO.File.ReadAllTextAsync(fullPath);
using var markdownStream = new System.IO.MemoryStream(System.Text.Encoding.UTF8.GetBytes(markdownContent)); using var markdownStream = new System.IO.MemoryStream(System.Text.Encoding.UTF8.GetBytes(markdownContent));
@ -87,7 +199,7 @@ public static class ApiV1Endpoints
var doc = new Aspose.Words.Document(markdownStream, loadOptions); var doc = new Aspose.Words.Document(markdownStream, loadOptions);
var outStream = new System.IO.MemoryStream(); var outStream = new System.IO.MemoryStream();
var saveFormat = mimeType == "application/pdf" ? Aspose.Words.SaveFormat.Pdf : Aspose.Words.SaveFormat.Docx; var saveFormat = mimeType == "application/pdf" ? Aspose.Words.SaveFormat.Pdf : (mimeType == "application/msword" ? Aspose.Words.SaveFormat.Doc : Aspose.Words.SaveFormat.Docx);
doc.Save(outStream, saveFormat); doc.Save(outStream, saveFormat);
outStream.Position = 0; outStream.Position = 0;
@ -120,11 +232,22 @@ public static class ApiV1Endpoints
var versions = records.Select(r => var versions = records.Select(r =>
{ {
var fullPath = System.IO.Path.Combine(basePath, $"{r.FileGuid}.version{r.Version}"); var fullPath = System.IO.Path.Combine(basePath, $"{r.FileGuid}.version{r.Version}");
var ext = System.IO.Path.GetExtension(latest.FileName);
var originalPath = System.IO.Path.Combine(basePath, $"{r.FileGuid}{ext}");
long size = 0; long size = 0;
if (System.IO.File.Exists(fullPath)) if (r.Version == 1 && System.IO.File.Exists(originalPath))
{
size = new System.IO.FileInfo(originalPath).Length;
}
else if (System.IO.File.Exists(fullPath))
{ {
size = new System.IO.FileInfo(fullPath).Length; size = new System.IO.FileInfo(fullPath).Length;
} }
else if (System.IO.File.Exists(originalPath))
{
size = new System.IO.FileInfo(originalPath).Length;
}
return new return new
{ {
@ -163,7 +286,7 @@ public static class ApiV1Endpoints
return Results.Ok(visibleHistory); return Results.Ok(visibleHistory);
} }
private static async Task<IResult> GenerateResponse(ChatRequest request, IConversationStorage storage, IConfiguration configuration, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) private static async Task<IResult> GenerateResponse(ChatRequest request, IConversationStorage storage, IConfiguration configuration, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory, IServiceProvider serviceProvider)
{ {
var logger = loggerFactory.CreateLogger("Henry.Server.Endpoints"); var logger = loggerFactory.CreateLogger("Henry.Server.Endpoints");
try try
@ -238,6 +361,9 @@ public static class ApiV1Endpoints
Message = agentResponse Message = agentResponse
}; };
var scopeFactory = serviceProvider.GetRequiredService<IServiceScopeFactory>();
GenerateConversationTitleBackground(scopeFactory, conversationId, endpointUrl, apiKey, modelName);
return Results.Ok(chatResponse); return Results.Ok(chatResponse);
} }
catch (Exception ex) catch (Exception ex)
@ -417,6 +543,9 @@ public static class ApiV1Endpoints
break; break;
} }
} }
var scopeFactory = response.HttpContext.RequestServices.GetRequiredService<IServiceScopeFactory>();
GenerateConversationTitleBackground(scopeFactory, conversationId, endpointUrl, apiKey, modelName);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -504,4 +633,84 @@ public static class ApiV1Endpoints
} }
return sb.ToString(); return sb.ToString();
} }
private class SkipImageSavingCallback : Aspose.Words.Saving.IImageSavingCallback
{
public void ImageSaving(Aspose.Words.Saving.ImageSavingArgs args)
{
args.ImageStream = new System.IO.MemoryStream();
args.KeepImageStreamOpen = false;
}
}
private static void GenerateConversationTitleBackground(IServiceScopeFactory scopeFactory, Guid conversationId, string endpointUrl, string apiKey, string modelName)
{
_ = Task.Run(async () =>
{
try
{
using var scope = scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AgentDbContext>();
var conversation = await db.Conversations.FirstOrDefaultAsync(c => c.Id == conversationId);
if (conversation == null) return;
if (conversation.Title != "New Conversation" && !conversation.Title.StartsWith("Conversation ")) return;
var messages = await db.Messages
.Where(m => m.ConversationId == conversationId && !m.IsInternal)
.OrderBy(m => m.CreatedAt)
.Take(4)
.ToListAsync();
if (!messages.Any()) return;
var files = await db.Files
.Where(f => f.ConversationId == conversationId && f.Version == 1)
.Select(f => f.FileName)
.ToListAsync();
var openAiMessages = new List<ChatMessage>
{
new SystemChatMessage("You are a helpful assistant. Generate a short, concise, and descriptive title (max 5-6 words) for the conversation. Do not use quotes. Just output the title text.")
};
if (files.Any())
{
openAiMessages.Add(new SystemChatMessage($"The user has uploaded the following files: {string.Join(", ", files)}"));
}
foreach (var m in messages)
{
if (m.Role.Equals("User", StringComparison.OrdinalIgnoreCase))
openAiMessages.Add(new UserChatMessage(m.Content));
else if (m.Role.Equals("Agent", StringComparison.OrdinalIgnoreCase))
openAiMessages.Add(new AssistantChatMessage(m.Content));
else
openAiMessages.Add(new UserChatMessage($"{m.Role}: {m.Content}"));
}
var clientOptions = new OpenAIClientOptions { Endpoint = new Uri(endpointUrl) };
var openAIClient = new OpenAIClient(new System.ClientModel.ApiKeyCredential(apiKey), clientOptions);
var chatClient = openAIClient.GetChatClient(modelName);
var completion = await chatClient.CompleteChatAsync(openAiMessages);
var title = completion.Value.Content[0].Text.Trim().Trim('"');
if (!string.IsNullOrEmpty(title))
{
var convToUpdate = await db.Conversations.FirstOrDefaultAsync(c => c.Id == conversationId);
if (convToUpdate != null && (convToUpdate.Title == "New Conversation" || convToUpdate.Title.StartsWith("Conversation ")))
{
convToUpdate.Title = title;
await db.SaveChangesAsync();
}
}
}
catch (Exception ex)
{
Console.WriteLine($"Failed to generate title: {ex.Message}");
}
});
}
} }

View File

@ -10,10 +10,11 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Aspose.Words" Version="26.6.0" /> <PackageReference Include="Aspose.Words" Version="26.6.0" />
<PackageReference Include="DotNetEnv" Version="3.2.0" /> <PackageReference Include="DotNetEnv" Version="3.2.0" />
<PackageReference Include="HarfBuzzSharp.NativeAssets.Linux" Version="3.119.0" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.7" /> <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.9" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.9" />
<PackageReference Include="OpenAI" Version="2.11.0" /> <PackageReference Include="OpenAI" Version="2.11.0" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="4.148.0" /> <PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="3.119.0" />
<PackageReference Include="SQLitePCLRaw.lib.e_sqlite3" Version="3.50.3" /> <PackageReference Include="SQLitePCLRaw.lib.e_sqlite3" Version="3.50.3" />
</ItemGroup> </ItemGroup>

View File

@ -0,0 +1,79 @@
@page
@model Henry.Server.Pages.AdminsModel
@{
Layout = "_DashboardLayout";
ViewData["Title"] = "System Administrators";
}
<div style="width: 100%; max-width: 1200px; margin: 0 auto;">
<div class="card">
<div style="display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border); padding-bottom: 16px; margin-bottom: 16px;">
<h2 style="margin: 0; font-size: 20px; font-weight: 600;">Admin Users</h2>
<a href="/Profile/New" style="background-color: var(--primary); color: #fff; text-decoration: none; padding: 8px 16px; border-radius: 4px; font-weight: 600; display: flex; align-items: center; gap: 8px;">
<span class="material-symbols-outlined" style="font-size: 18px;">add</span> Add Admin
</a>
</div>
@if (!string.IsNullOrEmpty(Model.ErrorMessage))
{
<div style="background-color: rgba(186, 26, 26, 0.1); color: var(--error); padding: 12px 16px; border-radius: 8px; margin-bottom: 24px; font-weight: 500;">
@Model.ErrorMessage
</div>
}
@if (!string.IsNullOrEmpty(Model.SuccessMessage))
{
<div style="background-color: rgba(34, 197, 94, 0.1); color: #15803d; padding: 12px 16px; border-radius: 8px; margin-bottom: 24px; font-weight: 500;">
@Model.SuccessMessage
</div>
}
@if (Model.Admins.Any())
{
<table>
<thead>
<tr>
<th>Username</th>
<th>Email</th>
<th>Created At</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@foreach (var admin in Model.Admins)
{
<tr>
<td style="font-weight: 600; display: flex; align-items: center; gap: 12px;">
<div style="width: 32px; height: 32px; border-radius: 50%; background-color: var(--primary-bg); color: var(--primary); display: flex; align-items: center; justify-content: center;">
<span class="material-symbols-outlined">person</span>
</div>
<a href="/Profile/@admin.Id" style="color: var(--primary); text-decoration: none;">@admin.Username</a>
</td>
<td>@admin.Email</td>
<td style="font-family: 'JetBrains Mono', monospace; font-size: 13px;">@admin.CreatedAt.ToString("g")</td>
<td>
<span style="background-color: rgba(34, 197, 94, 0.1); color: #15803d; padding: 4px 8px; border-radius: 4px; font-size: 12px; font-weight: 600; font-family: 'JetBrains Mono', monospace;">
ACTIVE
</span>
</td>
<td>
@if (admin.Id != Model.CurrentAdmin?.Id)
{
<form method="post" asp-page-handler="Delete" asp-route-id="@admin.Id" style="margin: 0; display: inline;">
<button type="submit" onclick="return confirm('Are you sure you want to delete admin @admin.Username?');" style="background: none; border: none; color: var(--error); cursor: pointer; display: flex; align-items: center; gap: 4px; padding: 4px 8px; border-radius: 4px; font-weight: 500; font-family: 'Inter', sans-serif;">
<span class="material-symbols-outlined" style="font-size: 18px;">delete</span>
</button>
</form>
}
</td>
</tr>
}
</tbody>
</table>
}
else
{
<p>No admins found.</p>
}
</div>
</div>

View File

@ -0,0 +1,70 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Threading.Tasks;
using Henry.Server.Storage;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
namespace Henry.Server.Pages;
[Authorize]
public class AdminsModel : PageModel
{
private readonly AgentDbContext _dbContext;
public AdminsModel(AgentDbContext dbContext)
{
_dbContext = dbContext;
}
public List<AdminEntity> Admins { get; set; } = new();
public AdminEntity? CurrentAdmin { get; set; }
public string? ErrorMessage { get; set; }
public string? SuccessMessage { get; set; }
public async Task OnGetAsync()
{
var currentUsername = User.Identity?.Name;
CurrentAdmin = await _dbContext.Admins.FirstOrDefaultAsync(a => a.Username == currentUsername);
Admins = await _dbContext.Admins
.OrderBy(a => a.Username)
.ToListAsync();
}
public async Task<IActionResult> OnPostDeleteAsync(System.Guid id)
{
var adminCount = await _dbContext.Admins.CountAsync();
if (adminCount <= 1)
{
ErrorMessage = "Cannot delete the last admin.";
await OnGetAsync();
return Page();
}
var admin = await _dbContext.Admins.FindAsync(id);
if (admin != null)
{
var currentUsername = User.Identity?.Name;
var currentAdmin = await _dbContext.Admins.FirstOrDefaultAsync(a => a.Username == currentUsername);
if (currentAdmin != null && admin.Id == currentAdmin.Id)
{
ErrorMessage = "You cannot delete yourself from this page. Ask another admin, or disable your own profile from the Profile settings.";
await OnGetAsync();
return Page();
}
_dbContext.Admins.Remove(admin);
await _dbContext.SaveChangesAsync();
SuccessMessage = $"Admin '{admin.Username}' was successfully deleted.";
}
await OnGetAsync();
return Page();
}
}

View File

@ -0,0 +1,149 @@
@page "{id:guid}"
@using System.Text.Json
@model Henry.Server.Pages.ConversationModel
@{
Layout = "_DashboardLayout";
ViewData["Title"] = $"Conversation: {Model.Conversation?.Title}";
}
<style>
.message-container {
display: flex;
flex-direction: column;
gap: 24px;
}
.message-box {
padding: 16px;
border-radius: 8px;
max-width: 80%;
font-family: 'Inter', sans-serif;
line-height: 1.5;
border: 1px solid var(--border);
}
.message-box.user {
align-self: flex-end;
background-color: var(--primary-bg);
border-color: var(--primary);
}
.message-box.agent {
align-self: flex-start;
background-color: var(--surface);
}
.message-box.system {
align-self: center;
background-color: rgba(0,0,0,0.05);
color: var(--text-muted);
max-width: 90%;
font-family: 'JetBrains Mono', monospace;
font-size: 13px;
}
.dark .message-box.system {
background-color: rgba(255,255,255,0.05);
}
.message-header {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
font-size: 13px;
font-weight: 600;
color: var(--text-muted);
}
.message-content {
white-space: pre-wrap;
}
</style>
<div style="width: 100%; max-width: 1200px; margin: 0 auto;">
<div style="margin-bottom: 24px;">
<a href="/Conversations" style="display: flex; align-items: center; gap: 8px; color: var(--text-muted); text-decoration: none;">
<span class="material-symbols-outlined" style="font-size: 18px;">arrow_back</span>
Back to Conversations
</a>
</div>
<div class="card">
<div style="border-bottom: 1px solid var(--border); padding-bottom: 16px; margin-bottom: 24px;">
<h2 style="margin: 0; font-size: 20px; font-weight: 600;">@Model.Conversation?.Title</h2>
<div style="font-family: 'JetBrains Mono', monospace; font-size: 13px; color: var(--text-muted); margin-top: 4px;">
ID: @Model.Conversation?.Id
</div>
</div>
<div class="message-container">
@if (Model.Conversation?.Messages != null && Model.Conversation.Messages.Any())
{
foreach (var msg in Model.Conversation.Messages)
{
var msgClass = msg.Role.ToLower() == "user" ? "user" : (msg.IsInternal || msg.Role.ToLower() == "system" ? "system" : "agent");
<div class="message-box @msgClass">
<div class="message-header">
<span>@(msg.Role.ToUpper()) @(msg.IsInternal ? "(INTERNAL)" : "")</span>
<span>@msg.CreatedAt.ToString("g")</span>
</div>
<div class="message-content">@msg.Content</div>
@{
if (msg.Role.ToLower() == "tool" || (msg.IsInternal && msg.Role.ToLower() == "system"))
{
try
{
using var doc = JsonDocument.Parse(msg.Content);
var root = doc.RootElement;
if (root.TryGetProperty("tool_result", out var toolResultEl) || root.TryGetProperty("ToolResult", out toolResultEl))
{
root = toolResultEl;
}
if (root.TryGetProperty("fileName", out var fileNameEl) || root.TryGetProperty("FileName", out fileNameEl))
{
if (root.TryGetProperty("fileGuid", out var fileGuidEl) || root.TryGetProperty("FileGuid", out fileGuidEl))
{
var dlFileName = fileNameEl.GetString();
var dlFileGuid = fileGuidEl.GetString();
string? dlVersion = null;
if (root.TryGetProperty("version", out var versionEl) || root.TryGetProperty("Version", out versionEl))
{
dlVersion = versionEl.ToString();
}
if (!string.IsNullOrEmpty(dlFileName) && !string.IsNullOrEmpty(dlFileGuid))
{
var dlUrl = $"/api/v1/files/{Model.Conversation?.Id}/{dlFileGuid}";
if (!string.IsNullOrEmpty(dlVersion))
{
dlUrl += $"/version{dlVersion}";
}
<div style="margin-top: 12px;">
<a href="@dlUrl" style="display: inline-flex; align-items: center; gap: 8px; background-color: var(--primary); color: var(--bg-color); padding: 6px 12px; border-radius: 16px; font-size: 13px; font-weight: 500; text-decoration: none;">
<span class="material-symbols-outlined" style="font-size: 16px;">download</span>
Download @dlFileName
@if (!string.IsNullOrEmpty(dlVersion))
{
<span style="opacity: 0.8; font-size: 11px;">(v@(dlVersion))</span>
}
</a>
</div>
}
}
}
}
catch { /* Not JSON or missing properties, ignore */ }
}
}
</div>
}
}
else
{
<p style="color: var(--text-muted); text-align: center;">No messages in this conversation.</p>
}
</div>
</div>
</div>

View File

@ -0,0 +1,37 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Henry.Server.Storage;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
namespace Henry.Server.Pages;
[Authorize]
public class ConversationModel : PageModel
{
private readonly AgentDbContext _dbContext;
public ConversationModel(AgentDbContext dbContext)
{
_dbContext = dbContext;
}
public ConversationEntity? Conversation { get; set; }
public async Task<IActionResult> OnGetAsync(Guid id)
{
Conversation = await _dbContext.Conversations
.Include(c => c.Messages.OrderBy(m => m.CreatedAt))
.FirstOrDefaultAsync(c => c.Id == id);
if (Conversation == null)
{
return NotFound();
}
return Page();
}
}

View File

@ -0,0 +1,44 @@
@page
@model Henry.Server.Pages.ConversationsModel
@{
Layout = "_DashboardLayout";
ViewData["Title"] = "Conversations History";
}
<div style="width: 100%; max-width: 1200px; margin: 0 auto;">
<div class="card">
<h2 class="card-title">All Conversations</h2>
@if (Model.Conversations.Any())
{
<table>
<thead>
<tr>
<th>Title</th>
<th>Conversation ID</th>
<th>Message Count</th>
<th>Last Activity</th>
</tr>
</thead>
<tbody>
@foreach (var conv in Model.Conversations)
{
<tr>
<td style="font-weight: 500;">
<a href="/Conversation/@conv.Id" style="text-decoration: none; color: var(--primary);">@conv.Title</a>
</td>
<td style="font-family: 'JetBrains Mono', monospace; font-size: 13px; color: var(--text-muted);">@conv.Id</td>
<td>@conv.Messages.Count</td>
<td style="color: var(--text-muted);">
@(conv.Messages.Any() ? conv.Messages.Max(m => m.CreatedAt).ToLocalTime().ToString("g") : "No messages")
</td>
</tr>
}
</tbody>
</table>
}
else
{
<p>No conversations have been recorded yet.</p>
}
</div>
</div>

View File

@ -0,0 +1,33 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Henry.Server.Storage;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
namespace Henry.Server.Pages;
[Authorize]
public class ConversationsModel : PageModel
{
private readonly AgentDbContext _dbContext;
public ConversationsModel(AgentDbContext dbContext)
{
_dbContext = dbContext;
}
public List<ConversationEntity> Conversations { get; set; } = new();
public async Task OnGetAsync()
{
var convos = await _dbContext.Conversations
.Include(c => c.Messages)
.ToListAsync();
Conversations = convos
.OrderByDescending(c => c.Messages.Max(m => (System.DateTime?)m.CreatedAt) ?? System.DateTime.MinValue)
.ToList();
}
}

View File

@ -0,0 +1,60 @@
@page
@model Henry.Server.Pages.FilesModel
@{
Layout = "_DashboardLayout";
ViewData["Title"] = "File Repository";
}
<div style="width: 100%; max-width: 1200px; margin: 0 auto;">
<div class="card">
<h2 class="card-title">Stored Files</h2>
@if (Model.Files.Any())
{
<table>
<thead>
<tr>
<th>File Name</th>
<th>Type</th>
<th>Version</th>
<th>Conversation</th>
<th>Created</th>
<th>Action</th>
</tr>
</thead>
<tbody>
@foreach (var file in Model.Files)
{
<tr>
<td style="font-weight: 500;">
<div>@file.FileName</div>
<div style="font-family: 'JetBrains Mono', monospace; font-size: 11px; color: var(--text-muted);">@file.FileGuid</div>
</td>
<td>
<span style="background-color: var(--primary-bg); color: var(--primary); padding: 4px 8px; border-radius: 4px; font-size: 12px; font-weight: 600;">
@(string.IsNullOrEmpty(file.MimeType) ? "Unknown" : file.MimeType)
</span>
</td>
<td>@file.Version</td>
<td style="font-size: 13px;">
<div>
<a href="/Conversation/@file.ConversationId" style="text-decoration: none; color: var(--primary);">@file.Conversation?.Title</a>
</div>
<div style="font-family: 'JetBrains Mono', monospace; font-size: 11px; color: var(--text-muted);">@file.ConversationId</div>
</td>
<td style="font-size: 13px;">@file.CreatedAt.ToLocalTime().ToString("g")</td>
<td>
<a href="/api/v1/files/@file.ConversationId/@file.FileGuid" style="display: flex; align-items: center; gap: 4px;">
<span class="material-symbols-outlined" style="font-size: 18px;">download</span>
</a>
</td>
</tr>
}
</tbody>
</table>
}
else
{
<p>No files are available in the repository.</p>
}
</div>
</div>

View File

@ -0,0 +1,30 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Henry.Server.Storage;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
namespace Henry.Server.Pages;
[Authorize]
public class FilesModel : PageModel
{
private readonly AgentDbContext _dbContext;
public FilesModel(AgentDbContext dbContext)
{
_dbContext = dbContext;
}
public List<FileEntity> Files { get; set; } = new();
public async Task OnGetAsync()
{
Files = await _dbContext.Files
.Include(f => f.Conversation)
.OrderByDescending(f => f.CreatedAt)
.ToListAsync();
}
}

View File

@ -0,0 +1,82 @@
@page
@model Henry.Server.Pages.IndexModel
@{
Layout = "_DashboardLayout";
ViewData["Title"] = "Dashboard";
}
<div style="width: 100%; max-width: 1200px; margin: 0 auto; display: grid; grid-template-columns: 1fr 1fr; gap: 32px;">
<div class="card">
<h2 class="card-title">Recent Conversations</h2>
@if (Model.Conversations.Any())
{
<table>
<thead>
<tr>
<th>Title</th>
<th>Msgs</th>
<th>Last Activity</th>
</tr>
</thead>
<tbody>
@foreach (var conv in Model.Conversations)
{
<tr>
<td>
<a href="/Conversation/@conv.Id" style="font-weight: 500; text-decoration: none; color: var(--primary);">@conv.Title</a>
<div style="font-family: 'JetBrains Mono', monospace; font-size: 12px; color: var(--text-muted);">@conv.Id</div>
</td>
<td>@conv.Messages.Count</td>
<td style="color: var(--text-muted); font-size: 13px;">
@(conv.Messages.Any() ? conv.Messages.Max(m => m.CreatedAt).ToLocalTime().ToString("g") : "No messages")
</td>
</tr>
}
</tbody>
</table>
}
else
{
<p>No conversations found.</p>
}
</div>
<div class="card">
<h2 class="card-title">Recent Files</h2>
@if (Model.Files.Any())
{
<table>
<thead>
<tr>
<th>File Name</th>
<th>Version</th>
<th>Uploaded</th>
<th>Action</th>
</tr>
</thead>
<tbody>
@foreach (var file in Model.Files)
{
<tr>
<td>
<div style="font-weight: 500;">@file.FileName</div>
<div style="font-family: 'JetBrains Mono', monospace; font-size: 12px; color: var(--text-muted);">@file.FileGuid</div>
</td>
<td>@file.Version</td>
<td style="color: var(--text-muted); font-size: 13px;">@file.CreatedAt.ToLocalTime().ToString("g")</td>
<td><a href="/api/v1/files/@file.ConversationId/@file.FileGuid">Download</a></td>
</tr>
}
</tbody>
</table>
}
else
{
<p>No files found.</p>
}
</div>
</div>
<div class="card" style="margin-top: 32px; max-width: 1200px; margin-left: auto; margin-right: auto;">
<h2 class="card-title">Server Logs</h2>
<p style="font-family: 'JetBrains Mono', monospace; font-size: 13px; color: var(--text-muted);">Server logging output is currently being captured via Console providers. Refer to standard output for live logs.</p>
</div>

View File

@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Henry.Server.Storage;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
namespace Henry.Server.Pages;
[Authorize]
public class IndexModel : PageModel
{
private readonly AgentDbContext _dbContext;
public IndexModel(AgentDbContext dbContext)
{
_dbContext = dbContext;
}
public List<ConversationEntity> Conversations { get; set; } = new();
public List<FileEntity> Files { get; set; } = new();
public async Task OnGetAsync()
{
var convos = await _dbContext.Conversations
.Include(c => c.Messages)
.ToListAsync();
Conversations = convos
.OrderByDescending(c => c.Messages.Max(m => (System.DateTime?)m.CreatedAt) ?? System.DateTime.MinValue)
.Take(5)
.ToList();
Files = await _dbContext.Files
.OrderByDescending(f => f.CreatedAt)
.Take(5)
.ToListAsync();
}
}

View File

@ -0,0 +1,42 @@
@page
@model Henry.Server.Pages.LoginModel
@{
ViewData["Title"] = "AgentOS Login";
}
<div class="ambient-glow"></div>
<div class="auth-wrapper">
<main class="auth-card">
<div class="text-center flex-col items-center gap-2">
<span class="material-symbols-outlined icon-logo">smart_toy</span>
<h1>Henry</h1>
<p class="subtitle">System Control Access</p>
</div>
<form method="post" class="flex-col gap-4">
<div class="flex-col gap-2">
<label for="username">Username</label>
<input id="username" type="text" asp-for="Username" placeholder="admin" required tabindex="1" />
</div>
<div class="flex-col gap-2">
<div class="flex justify-between items-center">
<label for="password">Password</label>
<a href="/PasswordRecovery" tabindex="4">Forgot?</a>
</div>
<input id="password" type="password" asp-for="Password" placeholder="••••••••" required tabindex="2" />
</div>
@if (!string.IsNullOrEmpty(Model.ErrorMessage))
{
<div class="error-msg">@Model.ErrorMessage</div>
}
<div class="mt-2">
<button type="submit" tabindex="3">
<span class="material-symbols-outlined">login</span>
Sign In
</button>
</div>
</form>
</main>
</div>

View File

@ -0,0 +1,69 @@
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using Henry.Server.Storage;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System;
namespace Henry.Server.Pages;
public class LoginModel : PageModel
{
private readonly AgentDbContext _dbContext;
public LoginModel(AgentDbContext dbContext)
{
_dbContext = dbContext;
}
[BindProperty]
public string Username { get; set; } = string.Empty;
[BindProperty]
public string Password { get; set; } = string.Empty;
public string ErrorMessage { get; set; } = string.Empty;
public void OnGet()
{
}
public async Task<IActionResult> OnPostAsync()
{
if (string.IsNullOrEmpty(Username) || string.IsNullOrEmpty(Password))
{
ErrorMessage = "Username and Password are required.";
return Page();
}
using var sha256 = SHA256.Create();
var bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(Password));
var hash = Convert.ToBase64String(bytes);
var user = _dbContext.Admins.FirstOrDefault(a => a.Username == Username && a.PasswordHash == hash);
if (user != null)
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, user.Username),
new Claim(ClaimTypes.Email, user.Email)
};
var identity = new ClaimsIdentity(claims, "Cookies");
var principal = new ClaimsPrincipal(identity);
await HttpContext.SignInAsync("Cookies", principal);
return RedirectToPage("/Index");
}
ErrorMessage = "Invalid username or password.";
return Page();
}
}

View File

@ -0,0 +1,4 @@
@page
@model Henry.Server.Pages.LogoutModel
@{
}

View File

@ -0,0 +1,15 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Henry.Server.Pages;
public class LogoutModel : PageModel
{
public async Task<IActionResult> OnPostAsync()
{
await HttpContext.SignOutAsync("Cookies");
return RedirectToPage("/Login");
}
}

View File

@ -0,0 +1,40 @@
@page
@model Henry.Server.Pages.PasswordRecoveryModel
@{
ViewData["Title"] = "Henry - Password Recovery";
}
<div class="ambient-glow"></div>
<div class="auth-wrapper">
<main class="auth-card">
<a href="/Login" class="flex items-center gap-2" style="color: var(--text-muted); align-self: flex-start;">
<span class="material-symbols-outlined" style="font-size: 20px;">arrow_back</span>
<span style="font-size: 14px; font-weight: 500;">Back to Login</span>
</a>
<div class="text-center flex-col items-center gap-2 mt-4">
<span class="material-symbols-outlined icon-logo">lock_reset</span>
<h1>Reset Password</h1>
<p class="subtitle">Enter your email to receive recovery instructions.</p>
</div>
<form method="post" class="flex-col gap-4">
<div class="flex-col gap-2">
<label for="email">Email Address</label>
<input id="email" type="email" asp-for="Email" placeholder="admin@example.com" required />
</div>
@if (!string.IsNullOrEmpty(Model.Message))
{
<div style="color: var(--primary); text-align: center; font-size: 14px;">@Model.Message</div>
}
<div class="mt-2">
<button type="submit">
<span class="material-symbols-outlined">send</span>
Send Recovery Link
</button>
</div>
</form>
</main>
</div>

View File

@ -0,0 +1,41 @@
using System.Linq;
using System.Threading.Tasks;
using Henry.Server.Storage;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Henry.Server.Pages;
public class PasswordRecoveryModel : PageModel
{
private readonly AgentDbContext _dbContext;
public PasswordRecoveryModel(AgentDbContext dbContext)
{
_dbContext = dbContext;
}
[BindProperty]
public string Email { get; set; } = string.Empty;
public string Message { get; set; } = string.Empty;
public void OnGet()
{
}
public IActionResult OnPost()
{
if (string.IsNullOrEmpty(Email))
{
Message = "Email is required.";
return Page();
}
var user = _dbContext.Admins.FirstOrDefault(a => a.Email == Email);
// Always show the same success message to prevent email enumeration
Message = "If that email is in our system, recovery instructions have been sent.";
return Page();
}
}

View File

@ -0,0 +1,103 @@
@page "{id?}"
@model Henry.Server.Pages.ProfileModel
@{
Layout = "_DashboardLayout";
ViewData["Title"] = Model.IsNew ? "Create Profile" : "Edit Profile";
}
<style>
.form-group {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 24px;
}
.form-group label {
font-weight: 600;
font-size: 14px;
}
.form-group input {
padding: 12px;
border-radius: 8px;
border: 1px solid var(--border);
background-color: var(--surface);
color: var(--text-color);
font-family: 'Inter', sans-serif;
}
.form-group input:focus {
outline: 2px solid var(--primary);
border-color: transparent;
}
.btn-submit {
background-color: var(--primary);
color: #fff;
border: none;
padding: 12px 24px;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.btn-submit:hover {
background-color: #004395; /* var(--primary-hover) */
}
.msg-error {
background-color: rgba(186, 26, 26, 0.1);
color: var(--error);
padding: 16px;
border-radius: 8px;
margin-bottom: 24px;
font-weight: 500;
}
.msg-success {
background-color: rgba(34, 197, 94, 0.1);
color: #15803d;
padding: 16px;
border-radius: 8px;
margin-bottom: 24px;
font-weight: 500;
}
</style>
<div style="width: 100%; max-width: 600px; margin: 0 auto;">
<div class="card">
<h2 class="card-title">@(Model.IsNew ? "New Admin" : "Profile Details")</h2>
@if (!string.IsNullOrEmpty(Model.ErrorMessage))
{
<div class="msg-error">@Model.ErrorMessage</div>
}
@if (!string.IsNullOrEmpty(Model.SuccessMessage))
{
<div class="msg-success">@Model.SuccessMessage</div>
}
<form method="post">
<input type="hidden" asp-for="TargetId" />
<input type="hidden" asp-for="IsNew" />
<div class="form-group">
<label for="Username">Username</label>
<input type="text" asp-for="Username" required />
</div>
<div class="form-group">
<label for="Email">Email</label>
<input type="email" asp-for="Email" />
</div>
<div class="form-group">
<label for="Password">Password @(Model.IsNew ? "*" : "(leave blank to keep current)")</label>
<input type="password" asp-for="Password" placeholder="••••••••" />
</div>
<button type="submit" class="btn-submit" style="width: 100%;">
<span class="material-symbols-outlined">save</span>
@(Model.IsNew ? "Create Admin" : "Save Changes")
</button>
</form>
</div>
</div>

View File

@ -0,0 +1,130 @@
using System;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using Henry.Server.Storage;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
namespace Henry.Server.Pages;
[Authorize]
public class ProfileModel : PageModel
{
private readonly AgentDbContext _dbContext;
public ProfileModel(AgentDbContext dbContext)
{
_dbContext = dbContext;
}
[BindProperty] public string? Username { get; set; }
[BindProperty] public string? Email { get; set; }
[BindProperty] public string? Password { get; set; }
[BindProperty] public Guid? TargetId { get; set; }
[BindProperty] public bool IsNew { get; set; }
public string? ErrorMessage { get; set; }
public string? SuccessMessage { get; set; }
public async Task<IActionResult> OnGetAsync(string? id)
{
if (string.Equals(id, "New", StringComparison.OrdinalIgnoreCase))
{
IsNew = true;
return Page();
}
AdminEntity? admin = null;
if (string.IsNullOrEmpty(id))
{
// Self profile
var currentUser = User.Identity?.Name;
if (string.IsNullOrEmpty(currentUser)) return RedirectToPage("/Login");
admin = await _dbContext.Admins.FirstOrDefaultAsync(a => a.Username == currentUser);
}
else if (Guid.TryParse(id, out var parsedGuid))
{
admin = await _dbContext.Admins.FirstOrDefaultAsync(a => a.Id == parsedGuid);
}
if (admin == null) return NotFound();
TargetId = admin.Id;
Username = admin.Username;
Email = admin.Email;
IsNew = false;
return Page();
}
public async Task<IActionResult> OnPostAsync()
{
if (string.IsNullOrWhiteSpace(Username))
{
ErrorMessage = "Username is required.";
return Page();
}
if (IsNew)
{
if (string.IsNullOrWhiteSpace(Password))
{
ErrorMessage = "Password is required for new users.";
return Page();
}
if (await _dbContext.Admins.AnyAsync(a => a.Username == Username))
{
ErrorMessage = "Username already exists.";
return Page();
}
var admin = new AdminEntity
{
Username = Username,
Email = Email ?? "",
PasswordHash = HashPassword(Password)
};
_dbContext.Admins.Add(admin);
await _dbContext.SaveChangesAsync();
TargetId = admin.Id;
IsNew = false;
SuccessMessage = "Profile created successfully.";
}
else
{
var admin = await _dbContext.Admins.FirstOrDefaultAsync(a => a.Id == TargetId);
if (admin == null) return NotFound();
if (admin.Username != Username && await _dbContext.Admins.AnyAsync(a => a.Username == Username && a.Id != TargetId))
{
ErrorMessage = "Username already exists.";
return Page();
}
admin.Username = Username;
admin.Email = Email ?? "";
if (!string.IsNullOrWhiteSpace(Password))
{
admin.PasswordHash = HashPassword(Password);
}
await _dbContext.SaveChangesAsync();
SuccessMessage = "Profile updated successfully.";
}
return Page();
}
private string HashPassword(string password)
{
using var sha256 = SHA256.Create();
var bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(password));
return Convert.ToBase64String(bytes);
}
}

View File

@ -0,0 +1,291 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - Henry Admin</title>
<link href="/css/fonts.css" rel="stylesheet" />
<style>
:root {
--bg-color: #f7f9fb;
--sidebar-bg: #f2f4f6;
--text-color: #191c1e;
--text-muted: #424754;
--primary: #0058be;
--primary-bg: #d0e1fb;
--surface: #ffffff;
--border: #c2c6d6;
--error: #ba1a1a;
--sidebar-width: 260px;
}
@@media (prefers-color-scheme: dark) {
:root {
--bg-color: #191c1e;
--sidebar-bg: #2d3133;
--text-color: #eff1f3;
--text-muted: #d8dadc;
--primary: #adc6ff;
--primary-bg: #38485d;
--surface: rgba(239, 241, 243, 0.1);
--border: #727785;
--error: #ffdad6;
}
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: 'Inter', sans-serif;
background-color: var(--bg-color);
color: var(--text-color);
display: flex;
min-height: 100vh;
overflow: hidden;
}
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}
.icon-fill { font-variation-settings: 'FILL' 1; }
.sidebar {
width: var(--sidebar-width);
background-color: var(--sidebar-bg);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
padding: 24px 0;
height: 100vh;
}
.sidebar-header {
padding: 0 24px;
margin-bottom: 32px;
display: flex;
align-items: center;
gap: 12px;
}
.logo-box {
width: 32px;
height: 32px;
background-color: var(--primary);
color: #ffffff;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
}
.sidebar-nav {
flex: 1;
padding: 0 16px;
display: flex;
flex-direction: column;
gap: 4px;
}
.nav-item {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
border-radius: 8px;
color: var(--text-muted);
text-decoration: none;
transition: all 0.2s;
font-size: 16px;
}
.nav-item:hover {
background-color: rgba(100,100,100,0.1);
color: var(--text-color);
}
.nav-item.active {
background-color: var(--primary-bg);
color: var(--primary);
border-left: 4px solid var(--primary);
font-weight: 600;
}
.nav-footer {
padding: 16px;
border-top: 1px solid var(--border);
display: flex;
flex-direction: column;
gap: 4px;
}
.nav-item.logout:hover {
color: var(--error);
background-color: rgba(186, 26, 26, 0.1);
}
.main-content {
flex: 1;
display: flex;
flex-direction: column;
height: 100vh;
overflow-y: auto;
}
/* Top Header */
.top-header {
height: 72px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 32px;
background-color: var(--bg-color);
position: sticky;
top: 0;
z-index: 10;
}
.user-dropdown {
position: relative;
display: inline-block;
}
.dropdown-content {
display: none;
position: absolute;
right: 0;
top: 100%;
background-color: var(--sidebar-bg);
min-width: 160px;
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.5);
border: 1px solid var(--border);
border-radius: 8px;
z-index: 100;
overflow: hidden;
}
.user-dropdown:hover .dropdown-content {
display: block;
}
.dropdown-item {
color: var(--text-color);
padding: 12px 16px;
text-decoration: none;
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-family: 'Inter', sans-serif;
transition: background-color 0.2s;
}
.dropdown-item:hover {
background-color: rgba(100,100,100,0.1);
}
.dropdown-item .material-symbols-outlined {
font-size: 18px;
}
.content-area {
padding: 32px;
flex: 1;
}
/* Reusable Card Styles for Dashboards */
.card {
background-color: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 24px;
}
.card-title {
font-size: 20px;
font-weight: 600;
margin-top: 0;
margin-bottom: 16px;
border-bottom: 1px solid var(--border);
padding-bottom: 12px;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid var(--border);
}
th {
color: var(--text-muted);
font-family: 'JetBrains Mono', monospace;
font-size: 13px;
text-transform: uppercase;
}
</style>
</head>
<body>
@{
var path = Context.Request.Path.Value;
}
<nav class="sidebar">
<div class="sidebar-header">
<div class="logo-box">
<span class="material-symbols-outlined icon-fill" style="font-size: 20px;">smart_toy</span>
</div>
<div>
<h1 style="margin: 0; font-size: 20px; color: var(--primary);">Henry Admin</h1>
<div style="font-size: 12px; color: var(--text-muted); font-family: 'JetBrains Mono', monospace;">v1.0.0-alpha</div>
</div>
</div>
<div class="sidebar-nav">
<a href="/Index" class="nav-item @(path == "/Index" || path == "/" ? "active" : "")">
<span class="material-symbols-outlined @(path == "/Index" || path == "/" ? "icon-fill" : "")">dashboard</span>
<span>Dashboard</span>
</a>
<a href="/Conversations" class="nav-item @(path == "/Conversations" ? "active" : "")">
<span class="material-symbols-outlined @(path == "/Conversations" ? "icon-fill" : "")">history</span>
<span>Conversations</span>
</a>
<a href="/Files" class="nav-item @(path == "/Files" ? "active" : "")">
<span class="material-symbols-outlined @(path == "/Files" ? "icon-fill" : "")">folder</span>
<span>Files</span>
</a>
<a href="/Admins" class="nav-item @(path == "/Admins" ? "active" : "")">
<span class="material-symbols-outlined @(path == "/Admins" ? "icon-fill" : "")">admin_panel_settings</span>
<span>Admins</span>
</a>
</div>
<div class="nav-footer">
<form method="post" asp-page="/Logout" style="margin: 0;">
<button type="submit" class="nav-item logout" style="width: 100%; border: none; background: transparent; cursor: pointer; text-align: left; font-size: 16px; font-family: 'Inter', sans-serif;">
<span class="material-symbols-outlined">logout</span>
<span>Log Out</span>
</button>
</form>
</div>
</nav>
<main class="main-content">
<header class="top-header">
<h2 style="margin: 0; font-size: 20px; font-weight: 600;">@ViewData["Title"]</h2>
<div class="user-dropdown">
<div style="display: flex; align-items: center; gap: 12px; color: var(--text-muted); cursor: pointer; padding: 8px 0;">
<span class="material-symbols-outlined">account_circle</span>
<span style="font-weight: 500;">@User.Identity?.Name</span>
<span class="material-symbols-outlined" style="font-size: 18px;">expand_more</span>
</div>
<div class="dropdown-content">
<a href="/Profile" class="dropdown-item">
<span class="material-symbols-outlined">person</span> Profile
</a>
<form method="post" asp-page="/Logout" style="margin: 0;">
<button type="submit" class="dropdown-item" style="width: 100%; border: none; background: transparent; cursor: pointer; text-align: left;">
<span class="material-symbols-outlined">logout</span> Log Out
</button>
</form>
</div>
</div>
</header>
<div class="content-area">
@RenderBody()
</div>
</main>
</body>
</html>

View File

@ -0,0 +1,181 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - Henry</title>
<link href="/css/fonts.css" rel="stylesheet" />
<style>
:root {
--bg-color: #f7f9fb;
--text-color: #191c1e;
--text-muted: #424754;
--primary: #0058be;
--primary-hover: #004395;
--primary-text: #ffffff;
--surface: #ffffff;
--border: #c2c6d6;
--input-focus: #005ac2;
--error: #ba1a1a;
}
@@media (prefers-color-scheme: dark) {
:root {
--bg-color: #191c1e;
--text-color: #eff1f3;
--text-muted: #d8dadc;
--primary: #adc6ff;
--primary-hover: #d8e2ff;
--primary-text: #001a42;
--surface: rgba(239, 241, 243, 0.1);
--border: #727785;
--input-focus: #adc6ff;
--error: #ffdad6;
}
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: 'Inter', sans-serif;
background-color: var(--bg-color);
color: var(--text-color);
display: flex;
flex-direction: column;
min-height: 100vh;
}
.material-symbols-outlined {
font-variation-settings: 'FILL' 1, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}
/* Layout for centered forms (Login / Recovery) */
.auth-wrapper {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}
.auth-card {
width: 100%;
max-width: 400px;
padding: 32px;
display: flex;
flex-direction: column;
gap: 24px;
}
.text-center { text-align: center; }
.flex { display: flex; }
.flex-col { display: flex; flex-direction: column; }
.items-center { align-items: center; }
.justify-center { justify-content: center; }
.justify-between { justify-content: space-between; }
.gap-2 { gap: 8px; }
.gap-4 { gap: 16px; }
.gap-6 { gap: 24px; }
.mt-2 { margin-top: 8px; }
.mt-4 { margin-top: 16px; }
h1 {
font-size: 32px;
font-weight: 600;
margin: 0;
line-height: 1.2;
letter-spacing: -0.02em;
}
p.subtitle {
font-size: 16px;
color: var(--text-muted);
margin: 0;
}
.icon-logo {
font-size: 48px;
color: var(--primary);
}
label {
font-family: 'JetBrains Mono', monospace;
font-size: 13px;
font-weight: 500;
letter-spacing: 0.05em;
text-transform: uppercase;
color: var(--text-muted);
}
input {
width: 100%;
background-color: var(--surface);
border: 1px solid var(--border);
border-radius: 4px;
padding: 8px 16px;
font-family: 'Inter', sans-serif;
font-size: 16px;
color: var(--text-color);
transition: all 0.2s ease-in-out;
outline: none;
}
input:focus {
border-color: var(--input-focus);
box-shadow: 0 0 0 1px var(--input-focus);
}
button {
width: 100%;
background-color: var(--primary);
color: var(--primary-text);
border: none;
border-radius: 4px;
padding: 12px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
button:hover {
background-color: var(--primary-hover);
}
a {
color: var(--primary);
text-decoration: none;
font-family: 'JetBrains Mono', monospace;
font-size: 13px;
}
a:hover { text-decoration: underline; }
.error-msg {
color: var(--error);
font-size: 14px;
text-align: center;
}
/* Ambient glow for background styling */
.ambient-glow {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 40vw;
height: 40vw;
background: radial-gradient(circle, rgba(173, 198, 255, 0.03) 0%, rgba(25, 28, 30, 0) 70%);
pointer-events: none;
z-index: -1;
}
</style>
</head>
<body>
@RenderBody()
</body>
</html>

View File

@ -0,0 +1,2 @@
@namespace Henry.Server.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

View File

@ -0,0 +1,3 @@
@{
Layout = "_Layout";
}

View File

@ -60,6 +60,13 @@ builder.Services.AddDbContext<AgentDbContext>(options =>
builder.Services.AddScoped<IConversationStorage, EfConversationStorage>(); builder.Services.AddScoped<IConversationStorage, EfConversationStorage>();
builder.Services.AddRazorPages();
builder.Services.AddAuthentication("Cookies").AddCookie("Cookies", options =>
{
options.LoginPath = "/Login";
});
builder.Services.AddAuthorization();
// Add services to the container. // Add services to the container.
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi(); builder.Services.AddOpenApi();
@ -72,14 +79,18 @@ if (app.Environment.IsDevelopment())
app.MapOpenApi(); app.MapOpenApi();
} }
if (!app.Environment.IsDevelopment())
{
app.UseHttpsRedirection(); app.UseHttpsRedirection();
}
app.UseStaticFiles();
// Authentication Middleware for HENRY_API_KEY // Authentication Middleware for HENRY_API_KEY (only for /api)
app.Use(async (context, next) => app.Use(async (context, next) =>
{ {
var apiKey = context.RequestServices.GetRequiredService<IConfiguration>()["HENRY_API_KEY"]; var apiKey = context.RequestServices.GetRequiredService<IConfiguration>()["HENRY_API_KEY"];
if (!string.IsNullOrEmpty(apiKey)) if (!string.IsNullOrEmpty(apiKey) && context.Request.Path.StartsWithSegments("/api"))
{ {
if (!context.Request.Headers.TryGetValue("Authorization", out var authHeader)) if (!context.Request.Headers.TryGetValue("Authorization", out var authHeader))
{ {
@ -111,8 +122,28 @@ using (var scope = app.Services.CreateScope())
{ {
var db = scope.ServiceProvider.GetRequiredService<AgentDbContext>(); var db = scope.ServiceProvider.GetRequiredService<AgentDbContext>();
db.Database.EnsureCreated(); db.Database.EnsureCreated();
// Seed default admin user
if (!db.Admins.Any())
{
using var sha256 = System.Security.Cryptography.SHA256.Create();
var bytes = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes("admin"));
var hash = Convert.ToBase64String(bytes);
db.Admins.Add(new AdminEntity
{
Username = "admin",
PasswordHash = hash,
Email = "admin@example.com"
});
db.SaveChanges();
}
} }
app.UseAuthentication();
app.UseAuthorization();
app.MapRazorPages();
app.MapApiV1Endpoints(); app.MapApiV1Endpoints();
app.Run(); app.Run();

View File

@ -35,11 +35,21 @@ public class FileEntity
public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
} }
public class AdminEntity
{
public Guid Id { get; set; } = Guid.NewGuid();
public string Username { get; set; } = string.Empty;
public string PasswordHash { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}
public class AgentDbContext : DbContext public class AgentDbContext : DbContext
{ {
public DbSet<ConversationEntity> Conversations { get; set; } = null!; public DbSet<ConversationEntity> Conversations { get; set; } = null!;
public DbSet<MessageEntity> Messages { get; set; } = null!; public DbSet<MessageEntity> Messages { get; set; } = null!;
public DbSet<FileEntity> Files { get; set; } = null!; public DbSet<FileEntity> Files { get; set; } = null!;
public DbSet<AdminEntity> Admins { get; set; } = null!;
public AgentDbContext(DbContextOptions<AgentDbContext> options) : base(options) { } public AgentDbContext(DbContextOptions<AgentDbContext> options) : base(options) { }
@ -60,5 +70,7 @@ public class AgentDbContext : DbContext
.WithMany() .WithMany()
.HasForeignKey(f => f.ConversationId) .HasForeignKey(f => f.ConversationId)
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<AdminEntity>().HasKey(a => a.Id);
} }
} }

View File

@ -12,6 +12,7 @@ public class SaveDocumentResult
{ {
public string? FileName { get; set; } public string? FileName { get; set; }
public string? FileGuid { get; set; } public string? FileGuid { get; set; }
public int? Version { get; set; }
public string? Error { get; set; } public string? Error { get; set; }
} }
@ -129,7 +130,7 @@ public static class FileTools
File.WriteAllText(fullPath, markdownContent); File.WriteAllText(fullPath, markdownContent);
return new SaveDocumentResult { FileName = fileName, FileGuid = targetGuid.ToString() }; return new SaveDocumentResult { FileName = fileName, FileGuid = targetGuid.ToString(), Version = version };
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -0,0 +1,267 @@
/* cyrillic-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(../fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2JL7SUc.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(../fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa0ZL7SUc.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(../fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2ZL7SUc.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(../fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1pL7SUc.woff2) format('woff2');
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(../fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2pL7SUc.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(../fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa25L7SUc.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(../fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(../fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2JL7SUc.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(../fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa0ZL7SUc.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(../fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2ZL7SUc.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(../fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1pL7SUc.woff2) format('woff2');
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(../fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2pL7SUc.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(../fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa25L7SUc.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(../fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(../fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2JL7SUc.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(../fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa0ZL7SUc.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(../fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2ZL7SUc.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(../fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1pL7SUc.woff2) format('woff2');
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(../fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2pL7SUc.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(../fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa25L7SUc.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(../fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(../fonts/tDbY2o-flEEny0FZhsfKu5WU4zr3E_BX0PnT8RD8-qxTN1OVgaY.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(../fonts/tDbY2o-flEEny0FZhsfKu5WU4zr3E_BX0PnT8RD8-qxTPlOVgaY.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek */
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(../fonts/tDbY2o-flEEny0FZhsfKu5WU4zr3E_BX0PnT8RD8-qxTOVOVgaY.woff2) format('woff2');
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* vietnamese */
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(../fonts/tDbY2o-flEEny0FZhsfKu5WU4zr3E_BX0PnT8RD8-qxTNVOVgaY.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(../fonts/tDbY2o-flEEny0FZhsfKu5WU4zr3E_BX0PnT8RD8-qxTNFOVgaY.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(../fonts/tDbY2o-flEEny0FZhsfKu5WU4zr3E_BX0PnT8RD8-qxTOlOV.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* fallback */
@font-face {
font-family: 'Material Symbols Outlined';
font-style: normal;
font-weight: 100 700;
font-display: swap;
src: url(../fonts/kJEPBvYX7BgnkSrUwT8OhrdQw4oELdPIeeII9v6oDMzBwG-RpA6RzaxHMPdY40KH8nGzv3fzfVJO1Q.woff2) format('woff2');
}
.material-symbols-outlined {
font-family: 'Material Symbols Outlined';
font-weight: normal;
font-style: normal;
font-size: 24px;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
-webkit-font-feature-settings: 'liga';
-webkit-font-smoothing: antialiased;
}