add rowboat app

This commit is contained in:
ramnique 2025-01-13 15:31:31 +05:30
parent b83b5f8a07
commit 10f76ef49f
117 changed files with 25370 additions and 0 deletions

View 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 &lt;/body&gt; 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&apos;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>;
}

View 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>
);
}

View 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} />;
}

View 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&apos;t be shown again!
</div>
)}
</div>
);
}

View 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>
);
}