Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
323 changes: 323 additions & 0 deletions web/assets/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,323 @@
@import "tailwindcss";
@plugin "@tailwindcss/typography";

/* SVGs */
:root {
--star-svg: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6"><path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z" /></svg>');

/* Shared colors */
--color-muted: theme(--color-zinc-400);
--color-accent: theme(--color-green-300);
--color-text: color-mix(in srgb, theme(--color-green-200) 80%, transparent);

/* Backgrounds */
--bg-page: theme(--color-zinc-800);
--bg-panel: color-mix(in srgb, theme(--color-zinc-900) 50%, transparent);
--bg-panel-hover: color-mix(
in srgb,
theme(--color-zinc-900) 80%,
transparent
);

/* Content colors */
--color-answer: color-mix(in srgb, theme(--color-zinc-100) 90%, transparent);
--color-link: color-mix(in srgb, theme(--color-green-400) 90%, transparent);
--color-link-hover: theme(--color-green-300);
--color-danger: theme(--color-red-400);
--color-danger-muted: color-mix(
in srgb,
theme(--color-red-400) 50%,
transparent
);
}

/* Custom scrollbar styling */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}

::-webkit-scrollbar-track {
background: var(--bg-page);
}

::-webkit-scrollbar-thumb {
background: color-mix(in srgb, var(--color-accent) 30%, transparent);
border-radius: 4px;
}

::-webkit-scrollbar-thumb:hover {
background: color-mix(in srgb, var(--color-accent) 50%, transparent);
}

/* Firefox */
* {
scrollbar-width: thin;
scrollbar-color: color-mix(in srgb, var(--color-accent) 30%, transparent)
var(--bg-page);
}

/* Controls panel */
.controls-panel {
@apply border-zinc-700/50 text-green-200 transition-colors;
background-color: var(--bg-panel);

&:hover {
background-color: var(--bg-panel-hover);
}

@media (width >= 99rem) {
@apply absolute top-0 left-0 h-full z-10;
}

h1 {
@apply text-lg font-light mt-5;
color: var(--color-muted);
}

h2 {
@apply text-base font-light;
color: var(--color-accent);
}

h3 {
@apply text-sm font-light;
color: var(--color-muted);
}

.controls-action-btn {
@apply outline-1 w-9 h-7 rounded-full transition-colors cursor-pointer;
&:hover {
background-color: var(--color-accent);
}
&:active {
@apply scale-90;
}
}
}

/* Controls section: collapsible */
.controls-section {
margin-bottom: 2rem;

&:not([open]) {
margin-bottom: 1rem;
}

summary {
@apply transition-opacity duration-300;
}

&:not([open]) summary {
@apply opacity-50;
}

.controls-section-chevron {
@apply transition-transform;
color: var(--color-muted);
}

&[open] .controls-section-chevron {
@apply rotate-90;
}

.controls-section-underline {
flex-basis: 100%;
height: 1px;
margin-top: 0.5rem;
background-color: var(--color-accent);
transform: scaleX(0);
transform-origin: left;
transition: transform 0.3s ease;
}

&[open] .controls-section-underline {
margin-bottom: 0.65rem;
transform: scaleX(1);
}

&[open] .controls-section-content {
animation: controls-fade-in 0.3s cubic-bezier(0.9, 0, 1, 1);
}
}

@keyframes controls-fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}

/* Bookmark group: collapsible */
.bookmark-group {
margin: 0.2rem 0.4rem 0 0.2rem;
@apply p-1;

summary {
@apply transition-opacity duration-300;
}

.bookmark-group-chevron {
@apply transition-transform;
color: var(--color-muted);
}

&[open] .bookmark-group-chevron {
@apply rotate-90;
}

input[type="checkbox"] {
appearance: none;
width: 11px;
height: 11px;
border-radius: 4px;
border: 1px solid var(--color-muted);
background-color: transparent;
cursor: pointer;
position: relative;
transition: border-color 0.15s;
flex-shrink: 0;
margin-left: 0.5px;
margin-top: 1.5px;

&:checked {
border-color: transparent;
}

&:checked::after {
content: "";
position: absolute;
bottom: 1px;
left: 2px;
width: 5px;
height: 9px;
border: solid var(--color-muted);
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}

&.checkbox-active:checked::after {
border-color: var(--color-accent);
}
}

&[open] > div {
animation: controls-fade-in 0.1s cubic-bezier(0.3, 0, 1, 1);
}
}

.bookmark-url {
color: inherit;
text-decoration: none;
}

.bookmark-url-active {
color: var(--color-link);
}

/* Controls row: label + control on a shared grid */
.controls-row {
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
gap: 1rem;
padding: 0.5rem 0.2rem;
}

.controls-row.stacked {
grid-template-columns: 1fr;
gap: 0.1rem;
padding-bottom: 0.5rem;
}

/* Slider thumb */
input[type="range"].slider-thumb-svg {
appearance: none;
background: transparent;
}

input[type="range"].slider-thumb-svg::-webkit-slider-runnable-track {
height: 4px;
border-radius: 2px;
background: var(--color-accent);
}

input[type="range"].slider-thumb-svg::-webkit-slider-thumb {
appearance: none;
width: 24px;
height: 24px;
margin-top: -10px;
border: none;
background-color: var(--color-accent);
-webkit-mask: var(--star-svg) no-repeat center / contain;
}

input[type="range"].slider-thumb-svg::-moz-range-track {
height: 1px;
border-radius: 2px;
background: color-mix(in srgb, var(--color-accent) 10%, transparent);
}

input[type="range"].slider-thumb-svg::-moz-range-thumb {
appearance: none;
width: 24px;
height: 24px;
border: none;
background-color: var(--color-accent);
mask: var(--star-svg) no-repeat center / contain;
}

/* Chat input */
.chat-input {
@apply bg-transparent focus:outline-none font-light text-lg resize-none
w-full;
color: var(--color-text);
caret-color: var(--color-accent);
}

/* Loading indicator */
.loading-indicator {
@apply my-5 px-5 py-3 rounded-lg;
background-color: var(--bg-panel);
}

/* Answer prose overrides */
.answer-prose {
@apply font-light max-w-none mt-5 pl-5 prose prose-invert prose-sm
prose-p:my-2 prose-p:leading-normal prose-li:leading-normal;
color: var(--color-answer);

:where(a) {
color: var(--color-link);
}

:where(code) {
color: var(--color-accent);
}

:where(pre) {
background-color: var(--bg-page);
}
}

/* Shared link style */
.link {
color: var(--color-link);
transition: color 0.15s;

&:hover {
color: var(--color-link-hover);
}
}

/* Danger button (delete actions) */
.danger-btn {
color: var(--color-danger-muted);
transition: color 0.15s;

&:hover {
color: var(--color-danger);
}
}
2 changes: 2 additions & 0 deletions web/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Import CSS files here for hot module reloading to work.
import "./assets/styles.css";
38 changes: 38 additions & 0 deletions web/components/Answer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { marked } from "marked";

export function Answer(props: { answer: string; references: string[] }) {
const html = marked.parse(props.answer) as string;

return (
<>
<div
class="answer-prose"
// Content comes from the trusted local backend; marked does not sanitize HTML
// deno-lint-ignore react-no-danger
dangerouslySetInnerHTML={{ __html: html }}
/>
{props.references.length > 0 && (
<div class="flex flex-col font-light mt-5 px-5 text-sm">
{props.references.map((ref, refIdx) => (
<a
key={refIdx}
href={ref}
target="_blank"
rel="noopener noreferrer"
draggable
onDragStart={(e) => {
e.dataTransfer?.setData("text/plain", ref);
if (e.dataTransfer) {
e.dataTransfer.effectAllowed = "copy";
}
}}
class="link cursor-grab active:cursor-grabbing"
>
[{refIdx + 1}] {ref}
</a>
))}
</div>
)}
</>
);
}
Loading
Loading