mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-13 17:22:37 +02:00
add rowboat app
This commit is contained in:
parent
b83b5f8a07
commit
10f76ef49f
117 changed files with 25370 additions and 0 deletions
124
apps/rowboat/app/projects/[projectId]/config/app.tsx
Normal file
124
apps/rowboat/app/projects/[projectId]/config/app.tsx
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
'use client';
|
||||
|
||||
import { Metadata } from "next";
|
||||
import { Secret } from "./secret";
|
||||
import { Divider, Spinner } from "@nextui-org/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Project } from "@/app/lib/types";
|
||||
import { getProjectConfig } from "@/app/actions";
|
||||
import { EmbedCode } from "./embed";
|
||||
import { WebhookUrl } from "./webhook-url";
|
||||
import { z } from 'zod';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Project config",
|
||||
};
|
||||
|
||||
export default function App({
|
||||
projectId,
|
||||
}: {
|
||||
projectId: string;
|
||||
}) {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [project, setProject] = useState<z.infer<typeof Project> | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let ignore = false;
|
||||
|
||||
async function fetchProjectConfig() {
|
||||
setIsLoading(true);
|
||||
const project = await getProjectConfig(projectId);
|
||||
if (!ignore) {
|
||||
setProject(project);
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
fetchProjectConfig();
|
||||
|
||||
return () => {
|
||||
ignore = true;
|
||||
};
|
||||
}, [projectId]);
|
||||
|
||||
const standardEmbedCode = `<!-- RowBoat Chat Widget -->
|
||||
<script>
|
||||
window.ROWBOAT_CONFIG = {
|
||||
clientId: '${project?.chatClientId}'
|
||||
};
|
||||
(function(d) {
|
||||
var s = d.createElement('script');
|
||||
s.src = 'https://chat.rowboatlabs.com/bootstrap.js';
|
||||
s.async = true;
|
||||
d.getElementsByTagName('head')[0].appendChild(s);
|
||||
})(document);
|
||||
</script>`;
|
||||
|
||||
const nextJsEmbedCode = `// Add this to your Next.js page or layout
|
||||
import Script from 'next/script'
|
||||
|
||||
export default function YourComponent() {
|
||||
return (
|
||||
<>
|
||||
<Script id="rowboat-config">
|
||||
{\`window.ROWBOAT_CONFIG = {
|
||||
clientId: '${project?.chatClientId}'
|
||||
};\`}
|
||||
</Script>
|
||||
<Script
|
||||
src="https://chat.rowboatlabs.com/bootstrap.js"
|
||||
strategy="lazyOnload"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}`
|
||||
|
||||
return <div className="flex flex-col h-full">
|
||||
<div className="shrink-0 flex justify-between items-center pb-4 border-b border-b-gray-100">
|
||||
<div className="flex flex-col">
|
||||
<h1 className="text-lg">Project config</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grow overflow-auto py-4">
|
||||
<div className="max-w-[768px] mx-auto">
|
||||
{isLoading && <div className="flex items-center gap-1">
|
||||
<Spinner size="sm" />
|
||||
<div>Loading project config...</div>
|
||||
</div>}
|
||||
{!isLoading && project && <div className="flex flex-col gap-4">
|
||||
<h2 className="font-semibold">Credentials</h2>
|
||||
<Secret
|
||||
initialSecret={project.secret}
|
||||
projectId={projectId}
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<h2 className="text-xl font-semibold">Add the chat widget to your website</h2>
|
||||
<p className="text-gray-600">Copy and paste this code snippet just before the closing </body> tag of your website:</p>
|
||||
<EmbedCode key="standard-embed-code" embedCode={standardEmbedCode} />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<h2 className="text-lg font-medium">Using Next.js?</h2>
|
||||
<p className="text-gray-600">If you're using Next.js, use this code instead:</p>
|
||||
<EmbedCode key="nextjs-embed-code" embedCode={nextJsEmbedCode} />
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold">Webhook settings</h2>
|
||||
<p className="mb-4">
|
||||
You can configure a webhook that will respond to tool calls.
|
||||
</p>
|
||||
<WebhookUrl
|
||||
initialUrl={project?.webhookUrl}
|
||||
projectId={projectId}
|
||||
/>
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
42
apps/rowboat/app/projects/[projectId]/config/embed.tsx
Normal file
42
apps/rowboat/app/projects/[projectId]/config/embed.tsx
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Textarea, Button } from "@nextui-org/react";
|
||||
import { CheckIcon, ClipboardIcon } from 'lucide-react';
|
||||
|
||||
interface EmbedCodeProps {
|
||||
embedCode: string;
|
||||
}
|
||||
|
||||
export function EmbedCode({ embedCode }: EmbedCodeProps) {
|
||||
const [isCopied, setIsCopied] = React.useState(false);
|
||||
|
||||
const handleCopy = () => {
|
||||
navigator.clipboard.writeText(embedCode);
|
||||
setIsCopied(true);
|
||||
setTimeout(() => setIsCopied(false), 1000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<Textarea
|
||||
labelPlacement="outside"
|
||||
variant="bordered"
|
||||
defaultValue={embedCode}
|
||||
className="max-w-full cursor-pointer"
|
||||
readOnly
|
||||
onClick={handleCopy}
|
||||
/>
|
||||
<div className="absolute bottom-2 right-2">
|
||||
<Button
|
||||
variant="flat"
|
||||
size="sm"
|
||||
onClick={handleCopy}
|
||||
isIconOnly
|
||||
>
|
||||
{isCopied ? <CheckIcon size={16} /> : <ClipboardIcon size={16} />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
15
apps/rowboat/app/projects/[projectId]/config/page.tsx
Normal file
15
apps/rowboat/app/projects/[projectId]/config/page.tsx
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { Metadata } from "next";
|
||||
import App from "./app";
|
||||
export const metadata: Metadata = {
|
||||
title: "Project config",
|
||||
};
|
||||
|
||||
export default function Page({
|
||||
params,
|
||||
}: {
|
||||
params: {
|
||||
projectId: string;
|
||||
};
|
||||
}) {
|
||||
return <App projectId={params.projectId} />;
|
||||
}
|
||||
94
apps/rowboat/app/projects/[projectId]/config/secret.tsx
Normal file
94
apps/rowboat/app/projects/[projectId]/config/secret.tsx
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
'use client';
|
||||
|
||||
import { Button, Input } from "@nextui-org/react";
|
||||
import { useState } from "react";
|
||||
import { rotateSecret } from "@/app/actions";
|
||||
import { CheckIcon, ClipboardIcon } from "lucide-react";
|
||||
|
||||
export function Secret({
|
||||
initialSecret,
|
||||
projectId
|
||||
}: {
|
||||
initialSecret: string,
|
||||
projectId: string
|
||||
}) {
|
||||
const getMaskedSecret = (secret: string) => {
|
||||
if (!secret) return '';
|
||||
if (secret.length <= 8) return secret;
|
||||
return `${secret.slice(0, 4)}${'•'.repeat(16)}${secret.slice(-4)}`;
|
||||
};
|
||||
|
||||
const [maskedSecret, setMaskedSecret] = useState(getMaskedSecret(initialSecret));
|
||||
const [showNewSecret, setShowNewSecret] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [showCopySuccess, setShowCopySuccess] = useState(false);
|
||||
|
||||
const handleRegenerate = async () => {
|
||||
if (!window.confirm('Are you sure you want to regenerate the webhook secret? This will invalidate the current secret key immediately.')) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const newSecret = await rotateSecret(projectId);
|
||||
setShowNewSecret(newSecret);
|
||||
setMaskedSecret(getMaskedSecret(newSecret));
|
||||
} catch (error) {
|
||||
console.error('Failed to regenerate webhook secret:', error);
|
||||
// You might want to add a toast or error message here
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopy = async () => {
|
||||
if (showNewSecret) {
|
||||
await navigator.clipboard.writeText(showNewSecret);
|
||||
setShowCopySuccess(true);
|
||||
setTimeout(() => {
|
||||
setShowCopySuccess(false);
|
||||
}, 1500);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<div className="text-sm text-gray-600 mb-2">Project Secret</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
<Input
|
||||
value={showNewSecret || maskedSecret}
|
||||
readOnly
|
||||
variant="bordered"
|
||||
className="font-mono"
|
||||
endContent={
|
||||
showNewSecret ? (
|
||||
<Button
|
||||
isIconOnly
|
||||
variant="light"
|
||||
onClick={handleCopy}
|
||||
>
|
||||
{showCopySuccess ? (
|
||||
<CheckIcon size={16} />
|
||||
) : (
|
||||
<ClipboardIcon size={16} />
|
||||
)}
|
||||
</Button>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
color="primary"
|
||||
variant="flat"
|
||||
onClick={handleRegenerate}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
Regenerate
|
||||
</Button>
|
||||
</div>
|
||||
{showNewSecret && (
|
||||
<div className="text-sm text-red-600 mt-2">
|
||||
Make sure to copy your new secret key. It won't be shown again!
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
81
apps/rowboat/app/projects/[projectId]/config/webhook-url.tsx
Normal file
81
apps/rowboat/app/projects/[projectId]/config/webhook-url.tsx
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
'use client';
|
||||
|
||||
import { Button, Input } from "@nextui-org/react";
|
||||
import { useState } from "react";
|
||||
import { updateWebhookUrl } from "@/app/actions";
|
||||
|
||||
export function WebhookUrl({
|
||||
initialUrl,
|
||||
projectId
|
||||
}: {
|
||||
initialUrl?: string,
|
||||
projectId: string
|
||||
}) {
|
||||
const [url, setUrl] = useState(initialUrl || '');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [showSuccess, setShowSuccess] = useState(false);
|
||||
|
||||
const handleUpdate = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
setShowSuccess(false);
|
||||
|
||||
// URL validation
|
||||
let parsedUrl;
|
||||
try {
|
||||
parsedUrl = new URL(url);
|
||||
} catch {
|
||||
setError('Please enter a valid URL');
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure HTTPS scheme
|
||||
if (parsedUrl.protocol !== 'https:') {
|
||||
setError('URL must use HTTPS');
|
||||
return;
|
||||
}
|
||||
|
||||
await updateWebhookUrl(projectId, url);
|
||||
setShowSuccess(true);
|
||||
setTimeout(() => {
|
||||
setShowSuccess(false);
|
||||
}, 3000);
|
||||
} catch (error) {
|
||||
console.error('Failed to update webhook URL:', error);
|
||||
setError('Failed to update webhook URL');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex gap-2 items-end">
|
||||
<Input
|
||||
label="Webhook URL"
|
||||
labelPlacement="outside"
|
||||
placeholder="https://example.com/webhook"
|
||||
value={url}
|
||||
onChange={(e) => {
|
||||
setUrl(e.target.value);
|
||||
setError(null);
|
||||
setShowSuccess(false);
|
||||
}}
|
||||
className="flex-grow"
|
||||
isInvalid={!!error}
|
||||
errorMessage={error}
|
||||
description={showSuccess ? "Webhook URL updated successfully" : undefined}
|
||||
/>
|
||||
<Button
|
||||
color="primary"
|
||||
onClick={handleUpdate}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue