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
10 changes: 5 additions & 5 deletions content/build/guides/application-distribution.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import Mermaid from "@/components/Mermaid";
import { Card, Cards } from "fumadocs-ui/components/card";
import { Globe, Package, Rocket, Shield } from "lucide-react";

**Overview:** This guide shows how to distribute software permanently using Arweave manifests and ArNS.
**Overview:** This guide shows how to distribute software permanently using <AskArieTooltip>Arweave manifests</AskArieTooltip> and ArNS.

You can publish binaries once and serve them from stable, human-readable URLs that keep working as releases change. Files are stored immutably on Arweave, old versions remain available, and links never break.

Expand Down Expand Up @@ -120,7 +120,7 @@ const dataItemOptions = {

### 3. Manifest-Based Routing

The system creates an Arweave manifest that provides structured routing for all binaries and metadata:
The system creates an <AskArieTooltip>Arweave manifest</AskArieTooltip> that provides structured routing for all binaries and metadata:

```typescript
const manifest: ArweaveManifest = {
Expand Down Expand Up @@ -294,7 +294,7 @@ The system leverages Nx for orchestrating the complex release pipeline:
**Pipeline Stages:**
1. **Build**: GoReleaser creates multi-platform binaries
2. **Upload**: Turbo SDK uploads compressed binaries to Arweave
3. **Manifest**: Creates routing manifest with all binary paths
3. **<AskArieTooltip term="Arweave manifest">Manifest</AskArieTooltip>**: Creates routing manifest with all binary paths
4. **ArNS**: Updates domain to point to new manifest
5. **Verification**: Tests installation script functionality

Expand All @@ -320,7 +320,7 @@ await ant.setRecord({

## Conclusion

This implementation demonstrates that Arweave manifests combined with ArNS provide a powerful alternative to traditional application distribution:
This implementation demonstrates that <AskArieTooltip>Arweave manifests</AskArieTooltip> combined with ArNS provide a powerful alternative to traditional application distribution:

- **Automated Release Pipeline**: CI/CD integration enables seamless multi-platform builds and deployment
- **Decentralized and Permanent Storage**: Applications are stored immutably across the Arweave network
Expand All @@ -335,7 +335,7 @@ This implementation demonstrates that Arweave manifests combined with ArNS provi
title="Manifests"
icon={<Package />}
>
Learn the fundamentals of Arweave manifest structure and creation.
Learn the fundamentals of <AskArieTooltip term="Arweave manifest">Arweave manifest</AskArieTooltip> structure and creation.
</Card>

<Card
Expand Down
7 changes: 5 additions & 2 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { RootProvider } from "fumadocs-ui/provider";
import type { Metadata } from "next";
import Script from "next/script";
import SearchDialog from "@/components/search";
import { AskArieProvider } from "@/components/ask-arie/AskArieContext";
import { AskArieWidget } from "@/components/ask-arie/AskArieWidget";
import { Plus_Jakarta_Sans } from "next/font/google";

Expand Down Expand Up @@ -48,8 +49,10 @@ export default function Layout({ children }: LayoutProps<"/">) {
enableSystem: true,
}}
>
{children}
<AskArieWidget />
<AskArieProvider>
{children}
<AskArieWidget />
</AskArieProvider>
</RootProvider>
</body>
</html>
Expand Down
33 changes: 33 additions & 0 deletions src/components/ask-arie/AskArieContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"use client";

import { createContext, useContext, useState, useCallback } from "react";

export interface AskArieContextValue {
/** null = not yet checked, true = healthy, false = unhealthy */
isHealthy: boolean | null;
setAskArieHealthy: (healthy: boolean) => void;
}

const AskArieContext = createContext<AskArieContextValue | null>(null);

export function AskArieProvider({ children }: { children: React.ReactNode }) {
const [isHealthy, setIsHealthy] = useState<boolean | null>(null);
const setAskArieHealthy = useCallback((healthy: boolean) => {
setIsHealthy(healthy);
}, []);
return (
<AskArieContext.Provider value={{ isHealthy, setAskArieHealthy }}>
{children}
</AskArieContext.Provider>
);
}

export function useAskArieHealth(): boolean | null {
const ctx = useContext(AskArieContext);
return ctx?.isHealthy ?? null;
}

export function useSetAskArieHealthy(): (healthy: boolean) => void {
const ctx = useContext(AskArieContext);
return ctx?.setAskArieHealthy ?? (() => {});
}
142 changes: 142 additions & 0 deletions src/components/ask-arie/AskArieTooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
"use client";

import { MessageCircle } from "lucide-react";
import { useCallback, useEffect, useRef, useState } from "react";
import { ASK_ARIE_OPEN_EVENT } from "@/lib/ask-arie";
import { useAskArieHealth } from "./AskArieContext";

export interface AskArieTooltipProps {
/** The visible text or content to wrap. Hovering shows the Ask Arie tooltip. */
children: React.ReactNode;
/**
* Optional. Text used to build the question sent to Arie (e.g. "ArNS").
* If omitted, string content from children is used when possible.
*/
term?: string;
}

function getTextFromChildren(children: React.ReactNode): string {
if (typeof children === "string") return children.trim();
if (Array.isArray(children)) return children.map(getTextFromChildren).join("").trim();
if (children != null && typeof children === "object" && "props" in children) {
const el = children as React.ReactElement<{ children?: React.ReactNode }>;
return getTextFromChildren(el.props.children);
}
return "";
}

const HIDE_DELAY_MS = 150;

export function AskArieTooltip({ children, term }: AskArieTooltipProps) {
const isHealthy = useAskArieHealth();
const [isVisible, setIsVisible] = useState(false);
const [tooltipPlaceAbove, setTooltipPlaceAbove] = useState(true);
const triggerRef = useRef<HTMLSpanElement>(null);
const hideTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);

const resolvedTerm = term ?? getTextFromChildren(children);
const question =
resolvedTerm.length > 0
? `What is the context and meaning of "${resolvedTerm}" on this page?`
: "What does this mean in the context of this page?";

const clearHideTimeout = useCallback(() => {
if (hideTimeoutRef.current !== null) {
clearTimeout(hideTimeoutRef.current);
hideTimeoutRef.current = null;
}
}, []);

const updatePosition = useCallback(() => {
const el = triggerRef.current;
if (el) {
const rect = el.getBoundingClientRect();
const spaceAbove = rect.top;
const spaceBelow = window.innerHeight - rect.bottom;
setTooltipPlaceAbove(spaceAbove >= spaceBelow);
}
}, []);

const showTooltip = useCallback(() => {
clearHideTimeout();
updatePosition();
setIsVisible(true);
}, [clearHideTimeout, updatePosition]);

const scheduleHide = useCallback(() => {
clearHideTimeout();
hideTimeoutRef.current = setTimeout(() => {
hideTimeoutRef.current = null;
setIsVisible(false);
}, HIDE_DELAY_MS);
}, [clearHideTimeout]);

useEffect(() => () => clearHideTimeout(), [clearHideTimeout]);

const handleAskArie = useCallback(() => {
clearHideTimeout();
window.dispatchEvent(
new CustomEvent(ASK_ARIE_OPEN_EVENT, {
detail: { question, autoSend: true },
})
);
setIsVisible(false);
}, [question, clearHideTimeout]);

const handleTriggerClick = useCallback(
(e: React.MouseEvent) => {
if ("ontouchstart" in window) {
e.preventDefault();
setIsVisible((v) => {
if (!v) updatePosition();
return !v;
});
}
},
[updatePosition]
);

if (isHealthy !== true) {
return <>{children}</>;
}

return (
<span
ref={triggerRef}
className="relative inline"
onMouseEnter={showTooltip}
onMouseLeave={scheduleHide}
onClick={handleTriggerClick}
>
<span className="border-b border-dotted border-fd-primary/50 cursor-help">
{children}
</span>
{isVisible && (
<span
className="absolute left-1/2 z-50 flex -translate-x-1/2 items-center gap-1.5 rounded-lg border border-fd-border bg-fd-popover px-2.5 py-1.5 text-sm text-fd-popover-foreground shadow-md whitespace-nowrap"
style={
tooltipPlaceAbove
? { bottom: "calc(100% + 6px)" }
: { top: "calc(100% + 6px)" }
}
role="tooltip"
onMouseEnter={showTooltip}
onMouseLeave={scheduleHide}
>
<button
type="button"
onClick={(e) => {
e.stopPropagation();
handleAskArie();
}}
className="inline-flex items-center gap-1.5 rounded-md bg-fd-primary px-2 py-1 text-xs font-medium text-fd-primary-foreground hover:bg-fd-primary/90 transition-colors"
aria-label={`Ask Arie about "${resolvedTerm || "this"}"`}
>
<MessageCircle className="size-3.5" />
Ask Arie
</button>
</span>
)}
</span>
);
}
60 changes: 56 additions & 4 deletions src/components/ask-arie/AskArieWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@ import { createPortal } from "react-dom";
import { Check, Copy, MessageCircle, RotateCcw, Send, X } from "lucide-react";
import {
askArie,
ASK_ARIE_OPEN_EVENT,
type AskArieOpenDetail,
ChatMessage,
checkAskArieHealth,
createAssistantMessageFromApiResponse,
generateMessageId,
} from "@/lib/ask-arie";
import { useSetAskArieHealthy } from "./AskArieContext";
import { MarkdownRenderer } from "./MarkdownRenderer";

interface AskArieWidgetProps {
Expand All @@ -37,6 +40,7 @@ export function AskArieWidget({ initialMessages = [] }: AskArieWidgetProps) {
const [copiedMessageId, setCopiedMessageId] = useState<string | null>(null);
const [mounted, setMounted] = useState(false);
const [isHealthy, setIsHealthy] = useState<boolean | null>(null);
const setAskArieHealthy = useSetAskArieHealthy();
const [threadId, setThreadId] = useState<string | null>(null);
const { resolvedTheme } = useTheme();
const [avatarSrc, setAvatarSrc] = useState("/brand/ask-arie.png");
Expand Down Expand Up @@ -105,14 +109,18 @@ export function AskArieWidget({ initialMessages = [] }: AskArieWidgetProps) {
const checkHealth = async () => {
const healthy = await checkAskArieHealth();
if (healthy) {
window.setTimeout(() => setIsHealthy(true), 1500);
window.setTimeout(() => {
setIsHealthy(true);
setAskArieHealthy(true);
}, 1500);
} else {
setIsHealthy(false);
setAskArieHealthy(false);
}
};

checkHealth();
}, [mounted]);
}, [mounted, setAskArieHealthy]);

useEffect(() => {
const el = messagesScrollRef.current;
Expand All @@ -129,12 +137,48 @@ export function AskArieWidget({ initialMessages = [] }: AskArieWidgetProps) {
return () => window.clearTimeout(id);
}, [messages, isLoading]);

const openChat = useCallback(() => {
const pendingAutoSendRef = useRef<string | null>(null);

const openChat = useCallback((initialQuestion?: string) => {
if (typeof initialQuestion === "string" && initialQuestion.trim()) {
setInputValue(initialQuestion.trim());
}
setIsOpen(true);
setTimeout(() => setIsVisible(true), 50);
setTimeout(() => inputRef.current?.focus(), 100);
}, []);

const startNewChatAndSend = useCallback((question: string) => {
const q = question.trim();
if (!q) return;
setMessages([]);
setThreadId(null);
setInputValue("");
sessionStorage.removeItem(ASK_ARIE_THREAD_ID_KEY);
sessionStorage.removeItem(ASK_ARIE_MESSAGES_KEY);
setError(null);
pendingAutoSendRef.current = q;
setIsOpen(true);
setTimeout(() => setIsVisible(true), 50);
setTimeout(() => inputRef.current?.focus(), 100);
}, []);

useEffect(() => {
const handleOpenWithQuestion = (e: Event) => {
const customEvent = e as CustomEvent<AskArieOpenDetail>;
const detail = customEvent.detail;
const question = detail?.question;
if (typeof question !== "string") return;
if (detail?.autoSend) {
startNewChatAndSend(question);
} else {
openChat(question);
}
};
window.addEventListener(ASK_ARIE_OPEN_EVENT, handleOpenWithQuestion);
return () => window.removeEventListener(ASK_ARIE_OPEN_EVENT, handleOpenWithQuestion);
}, [openChat, startNewChatAndSend]);

const closeChat = useCallback(() => {
setIsVisible(false);
setTimeout(() => setIsOpen(false), 200);
Expand Down Expand Up @@ -220,6 +264,14 @@ export function AskArieWidget({ initialMessages = [] }: AskArieWidgetProps) {
[threadId]
);

useEffect(() => {
if (!isOpen) return;
const question = pendingAutoSendRef.current;
if (!question) return;
pendingAutoSendRef.current = null;
sendMessage(question);
}, [isOpen, sendMessage]);

const handleSubmit = useCallback(
async (e: React.FormEvent) => {
e.preventDefault();
Expand All @@ -245,7 +297,7 @@ export function AskArieWidget({ initialMessages = [] }: AskArieWidgetProps) {
isHealthy === true ? (
<button
type="button"
onClick={openChat}
onClick={() => openChat()}
className="group fixed bottom-4 right-4 z-50 inline-flex h-12 w-12 cursor-pointer items-center justify-center rounded-full bg-fd-primary text-fd-background shadow-lg hover:bg-fd-primary/90 hover:scale-110 active:scale-95 transition-transform duration-150 ease-out"
aria-label="Open chat"
>
Expand Down
10 changes: 10 additions & 0 deletions src/lib/ask-arie.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,16 @@ export async function checkTechnicalAccess(): Promise<boolean> {
}
}

/** Payload for the Ask Arie open event. */
export interface AskArieOpenDetail {
question: string;
/** If true, start a new chat and send the question immediately. */
autoSend?: boolean;
}

/** Custom event name to open the Ask Arie widget (detail: AskArieOpenDetail). */
export const ASK_ARIE_OPEN_EVENT = "ask-arie-open";

export async function checkAskArieHealth(): Promise<boolean> {
try {
const response = await fetch(HEALTH_ENDPOINT, {
Expand Down
2 changes: 2 additions & 0 deletions src/mdx-components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import defaultMdxComponents from "fumadocs-ui/mdx";
import type { MDXComponents } from "mdx/types";
import Tip from "@/components/Tip";
import { CodeGroup } from "@/components/Code";
import { AskArieTooltip } from "@/components/ask-arie/AskArieTooltip";
import Mermaid from "@/components/Mermaid";
import { openapi } from "@/lib/openapi";
import { APIPage } from "fumadocs-openapi/ui";
Expand Down Expand Up @@ -37,6 +38,7 @@ export function getMDXComponents(components?: MDXComponents): MDXComponents {
...defaultMdxComponents,
Tip,
CodeGroup,
AskArieTooltip,
Mermaid,
Step,
Steps,
Expand Down