Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,13 +158,13 @@ askable-ui is the context layer. It doesn't replace your LLM SDK — it gives it
| Package | Version | Use when |
|---|---|---|
| [`@askable-ui/core`](https://www.npmjs.com/package/@askable-ui/core) | [![npm](https://img.shields.io/npm/v/@askable-ui/core?color=4f46e5)](https://www.npmjs.com/package/@askable-ui/core) | Vanilla JS, custom framework, or as a peer dep |
| [`@askable-ui/context`](https://www.npmjs.com/package/@askable-ui/context) | npm package | Shared Context packet types, schema, and validators |
| [`@askable-ui/context`](https://www.npmjs.com/package/@askable-ui/context) | [![npm](https://img.shields.io/npm/v/@askable-ui/context?color=4f46e5)](https://www.npmjs.com/package/@askable-ui/context) | Shared Context packet types, schema, and validators |
| [`@askable-ui/react`](https://www.npmjs.com/package/@askable-ui/react) | [![npm](https://img.shields.io/npm/v/@askable-ui/react?color=4f46e5)](https://www.npmjs.com/package/@askable-ui/react) | React 18+ |
| [`@askable-ui/react-native`](https://www.npmjs.com/package/@askable-ui/react-native) | [![npm](https://img.shields.io/npm/v/@askable-ui/react-native?color=4f46e5)](https://www.npmjs.com/package/@askable-ui/react-native) | React Native (initial press-driven adapter) |
| [`@askable-ui/vue`](https://www.npmjs.com/package/@askable-ui/vue) | [![npm](https://img.shields.io/npm/v/@askable-ui/vue?color=4f46e5)](https://www.npmjs.com/package/@askable-ui/vue) | Vue 3 |
| [`@askable-ui/svelte`](https://www.npmjs.com/package/@askable-ui/svelte) | [![npm](https://img.shields.io/npm/v/@askable-ui/svelte?color=4f46e5)](https://www.npmjs.com/package/@askable-ui/svelte) | Svelte 4 & 5 |
| [`@askable-ui/mcp`](https://www.npmjs.com/package/@askable-ui/mcp) | npm package | MCP bridge for exposing Context packets to agents |
| [`@askable-ui/create-app`](https://www.npmjs.com/package/@askable-ui/create-app) | npm package | React + Vite + CopilotKit starter scaffold |
| [`@askable-ui/mcp`](https://www.npmjs.com/package/@askable-ui/mcp) | [![npm](https://img.shields.io/npm/v/@askable-ui/mcp?color=4f46e5)](https://www.npmjs.com/package/@askable-ui/mcp) | MCP bridge for exposing Context packets to agents |
| [`@askable-ui/create-app`](https://www.npmjs.com/package/@askable-ui/create-app) | [![npm](https://img.shields.io/npm/v/@askable-ui/create-app?color=4f46e5)](https://www.npmjs.com/package/@askable-ui/create-app) | React + Vite + CopilotKit starter scaffold |

<details>
<summary><strong>Framework quick starts</strong></summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,14 @@ export function AskableInteractionToolbar() {
ctx,
source: { app: "analytics-dashboard-demo" },
onCapture(packet, selection) {
const label = `Highlighted text: ${selection.text}`
ctx.push(
{
capture: packet.capture.mode,
gesture: packet.capture.gesture ?? "programmatic",
length: selection.text.length,
...(selection.bounds ? { bounds: selection.bounds } : {}),
},
label,
selection.text,
)
setStatus(`Sent ${selection.text.length} selected characters to chat context.`)
resumeImplicitFocus()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client"

import { useState, useEffect } from "react"
import { Send, Sparkles, X, Maximize2, Minimize2, Eye } from "lucide-react"
import { Send, Sparkles, X, Maximize2, Minimize2, Eye, Quote } from "lucide-react"
import { useAskable } from "@askable-ui/react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
Expand All @@ -15,6 +15,8 @@ interface Message {
timestamp: Date
context?: string
isContext?: boolean
contextKind?: "text" | "ui"
selectedText?: string
}

const initialMessages: Message[] = [
Expand All @@ -33,7 +35,9 @@ export function ChatSidebar() {
const [isMinimized, setIsMinimized] = useState(false)
const [showContext, setShowContext] = useState(true)

const { promptContext } = useAskable({ inspector: true });
const { promptContext, focus } = useAskable({ inspector: true })
const hasSelectedText = isTextSelectionMeta(focus?.meta)
const selectedText = hasSelectedText ? focus?.text ?? "" : ""

const handleSend = () => {
if (!input.trim()) return
Expand All @@ -44,6 +48,8 @@ export function ChatSidebar() {
content: input,
timestamp: new Date(),
context: promptContext || undefined,
contextKind: hasSelectedText ? "text" : "ui",
selectedText,
}

setMessages((prev) => [...prev, userMessage])
Expand All @@ -56,6 +62,8 @@ export function ChatSidebar() {
content: promptContext,
timestamp: new Date(),
isContext: true,
contextKind: hasSelectedText ? "text" : "ui",
selectedText,
}
setMessages((prev) => [...prev, assistantMessage])
}
Expand Down Expand Up @@ -119,26 +127,43 @@ export function ChatSidebar() {
{/* Context Panel - Main Feature Showcase */}
<div className="border-b border-border">
{promptContext ? (
<div className="bg-gradient-to-b from-emerald-500/10 to-emerald-500/5 p-4">
<div className={cn(
"p-4",
hasSelectedText
? "bg-gradient-to-b from-violet-500/10 to-violet-500/5"
: "bg-gradient-to-b from-emerald-500/10 to-emerald-500/5"
)}>
<div className="mb-3 flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="flex h-7 w-7 items-center justify-center rounded-full bg-emerald-500">
<Eye className="h-4 w-4 text-white" />
<div className={cn(
"flex h-7 w-7 items-center justify-center rounded-full",
hasSelectedText ? "bg-violet-500" : "bg-emerald-500"
)}>
{hasSelectedText ? <Quote className="h-4 w-4 text-white" /> : <Eye className="h-4 w-4 text-white" />}
</div>
<div>
<span className="text-sm font-semibold text-emerald-400">Context Captured</span>
<p className="text-xs text-emerald-400/70">AI can see this element</p>
<span className={cn("text-sm font-semibold", hasSelectedText ? "text-violet-400" : "text-emerald-400")}>
{hasSelectedText ? "Selected Text Captured" : "Context Captured"}
</span>
<p className={cn("text-xs", hasSelectedText ? "text-violet-400/70" : "text-emerald-400/70")}>
{hasSelectedText ? "This exact highlight will be sent to chat" : "AI can see this element"}
</p>
</div>
</div>
<button
onClick={() => setShowContext(!showContext)}
className="rounded-md border border-emerald-500/30 bg-emerald-500/10 px-2 py-1 text-xs font-medium text-emerald-400 transition-colors hover:bg-emerald-500/20"
className={cn(
"rounded-md border px-2 py-1 text-xs font-medium transition-colors",
hasSelectedText
? "border-violet-500/30 bg-violet-500/10 text-violet-400 hover:bg-violet-500/20"
: "border-emerald-500/30 bg-emerald-500/10 text-emerald-400 hover:bg-emerald-500/20"
)}
>
{showContext ? "Collapse" : "Expand"}
</button>
</div>
{showContext && (
<ContextPanel content={promptContext} />
<ContextPanel content={promptContext} selectedText={selectedText} />
)}
</div>
) : (
Expand Down Expand Up @@ -176,13 +201,20 @@ export function ChatSidebar() {
)}
<div className="max-w-[85%]">
{message.context && message.role === "user" && (
<div className="mb-1 flex items-center gap-1 text-xs text-chart-1">
<Eye className="h-3 w-3" />
<span>with context</span>
<div className={cn(
"mb-1 flex items-center gap-1 text-xs",
message.contextKind === "text" ? "text-violet-400" : "text-chart-1"
)}>
{message.contextKind === "text" ? <Quote className="h-3 w-3" /> : <Eye className="h-3 w-3" />}
<span>{message.contextKind === "text" ? "with selected text" : "with context"}</span>
</div>
)}
{message.isContext ? (
<ContextCard content={message.content} />
<ContextCard
content={message.content}
kind={message.contextKind ?? "ui"}
selectedText={message.selectedText}
/>
) : (
<div
className={cn(
Expand Down Expand Up @@ -247,6 +279,14 @@ function parseContext(content: string): Record<string, unknown> | string[] | nul
return lines.length > 1 ? lines : null
}

function isTextSelectionMeta(meta: unknown) {
return Boolean(
meta &&
typeof meta === "object" &&
(meta as Record<string, unknown>).capture === "text-selection"
)
}

function renderContextValue(value: unknown, tone: "panel" | "card" = "panel") {
if (value !== null && typeof value === "object") {
return (
Expand All @@ -266,7 +306,28 @@ function renderContextValue(value: unknown, tone: "panel" | "card" = "panel") {
return <span className="text-xs font-medium text-foreground break-words whitespace-pre-wrap">{String(value)}</span>
}

function ContextPanel({ content }: { content: string }) {
function SelectedTextPreview({ text, tone = "panel" }: { text: string; tone?: "panel" | "card" }) {
return (
<div className={cn(
"rounded-lg border p-3",
tone === "panel"
? "border-violet-500/20 bg-background/80"
: "border-violet-500/30 bg-violet-500/10"
)}>
<div className="mb-2 flex items-center gap-1.5 text-xs font-semibold text-violet-400">
<Quote className="h-3.5 w-3.5" />
<span>Selected text passed to chat</span>
</div>
<blockquote className="text-xs leading-relaxed text-foreground break-words whitespace-pre-wrap">
{text}
</blockquote>
</div>
)
}

function ContextPanel({ content, selectedText }: { content: string; selectedText?: string }) {
if (selectedText) return <SelectedTextPreview text={selectedText} />

const parsed = parseContext(content)

if (parsed && !Array.isArray(parsed)) {
Expand Down Expand Up @@ -299,7 +360,17 @@ function ContextPanel({ content }: { content: string }) {
)
}

function ContextCard({ content }: { content: string }) {
function ContextCard({
content,
kind = "ui",
selectedText,
}: {
content: string
kind?: "text" | "ui"
selectedText?: string
}) {
if (kind === "text" && selectedText) return <SelectedTextPreview text={selectedText} tone="card" />

const parsed = parseContext(content)
const entries = parsed && !Array.isArray(parsed) ? Object.entries(parsed) : null
const lines = Array.isArray(parsed) ? parsed : content.split(/\s*—\s*/).filter(Boolean)
Expand Down
2 changes: 1 addition & 1 deletion examples/analytics-dashboard-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"start": "next start"
},
"dependencies": {
"@askable-ui/react": "^0.11.1",
"@askable-ui/react": "^0.12.0",
"@hookform/resolvers": "^3.9.1",
"@radix-ui/react-accordion": "1.2.12",
"@radix-ui/react-alert-dialog": "1.1.15",
Expand Down
6 changes: 3 additions & 3 deletions examples/react-native-expo/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@askable-ui/example-react-native-expo",
"private": true,
"version": "0.0.0",
"version": "0.12.0",
"main": "expo/AppEntry",
"scripts": {
"start": "expo start",
Expand All @@ -11,8 +11,8 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@askable-ui/core": "^0.11.1",
"@askable-ui/react-native": "^0.11.1",
"@askable-ui/core": "^0.12.0",
"@askable-ui/react-native": "^0.12.0",
"@react-navigation/native": "^7.2.5",
"@react-navigation/native-stack": "^7.16.0",
"expo": "^55.0.26",
Expand Down
34 changes: 17 additions & 17 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "askable",
"version": "0.11.1",
"version": "0.12.0",
"private": true,
"workspaces": [
"packages/*"
Expand Down
2 changes: 1 addition & 1 deletion packages/context/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@askable-ui/context",
"version": "0.11.1",
"version": "0.12.0",
"description": "Open Context packet types and schema for AI-native interfaces",
"type": "module",
"main": "./dist/index.js",
Expand Down
2 changes: 2 additions & 0 deletions packages/context/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export interface WebContextSurrounding {
nearby?: WebContextTarget[];
visible?: WebContextTarget[];
history?: WebContextTarget[];
sources?: WebContextTarget[];
}

export interface WebContextPrivacy {
Expand Down Expand Up @@ -180,6 +181,7 @@ export const webContextPacketSchema = {
nearby: { type: 'array', items: { $ref: '#/$defs/target' } },
visible: { type: 'array', items: { $ref: '#/$defs/target' } },
history: { type: 'array', items: { $ref: '#/$defs/target' } },
sources: { type: 'array', items: { $ref: '#/$defs/target' } },
},
},
privacy: {
Expand Down
Loading
Loading