diff --git a/.firebaserc b/.firebaserc index 97d37bd..4b940bf 100644 --- a/.firebaserc +++ b/.firebaserc @@ -3,9 +3,6 @@ "targets": { "docs-and-blog": { "hosting": { - "docs": [ - "gumnut-docs" - ], "blog": [ "gumnut-blog" ] @@ -13,4 +10,4 @@ } }, "etags": {} -} \ No newline at end of file +} diff --git a/docs/.vitepress/case-studies.data.mts b/docs/.vitepress/case-studies.data.mts new file mode 100644 index 0000000..b847817 --- /dev/null +++ b/docs/.vitepress/case-studies.data.mts @@ -0,0 +1,19 @@ +import { createContentLoader } from "vitepress"; + +export default createContentLoader("case-studies/**/*.md", { + excerpt: true, + transform(raw) { + return raw + .map(({ url, frontmatter, excerpt }) => ({ + title: frontmatter.title, + url, + excerpt, + date: frontmatter.date, + tags: frontmatter.tags || [], + author: frontmatter.author, + description: frontmatter.description, + image: frontmatter.image, + })) + .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); + }, +}); diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index d4729e9..2513b97 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -1,25 +1,62 @@ import { defineConfig } from "vitepress"; +import { getFeed } from "./getFeed"; +import { createContentLoader } from "vitepress"; +import implicitFigures from "markdown-it-implicit-figures"; + +// Helper function to parse dates consistently +function parseDate(dateStr: any): Date { + if (!dateStr) { + return new Date(0); // Return a very old date if no date is provided + } + if (typeof dateStr !== "string") { + return new Date(dateStr); // Try to parse whatever we got + } + // If the date string is already in ISO format, use it directly + if (dateStr.includes("T")) { + return new Date(dateStr); + } + // Otherwise, assume it's in YYYY-MM-DD format and add time + return new Date(`${dateStr}T00:00:00.000Z`); +} // https://vitepress.dev/reference/site-config export default defineConfig({ - title: "Docs", - description: "Documentation for Gumnut - the modern textbox", + title: "Gumnut 🦩", + description: "Modern SaaS for teams", + ignoreDeadLinks: true, cleanUrls: true, themeConfig: { // https://vitepress.dev/reference/default-theme-config + siteTitle: false, + externalLinkIcon: false, logo: { light: "/logo/light.svg", dark: "/logo/dark.svg", alt: "Gumnut Logo", }, - // nav: [{ text: "Support", link: "mailto:hello@gumnut.dev" }], + nav: [ + { text: "Pricing", link: "/pricing" }, + { + text: "Resources", + items: [ + { text: "Blog", link: "/blog" }, + { text: "Questions Hub", link: "/questions" }, + ], + }, + { text: "Case Studies", link: "/case-studies" }, + { text: "Docs", link: "/docs" }, + { text: "Login", link: "https://dashboard.gumnut.dev" }, + { text: "Book a demo", link: "https://calendly.com/owen-gumnut/30min" }, + ], socialLinks: [ - { icon: "discord", link: "https://discord.gg/yu3u87AUNR" }, - { icon: "github", link: "https://github.com/gumnutdev" }, { icon: "linkedin", link: "https://www.linkedin.com/company/gumnut-dev/", }, + { + icon: "discord", + link: "https://discord.gg/yu3u87AUNR", + }, ], sidebar: [ { @@ -28,33 +65,33 @@ export default defineConfig({ { text: "Get Started", items: [ - { text: "Introduction", link: "/introduction" }, - { text: "Quick Start", link: "/quickstart" }, - { text: "Walkthrough", link: "/walkthrough" }, + { text: "Introduction", link: "/docs/" }, + { text: "Quick Start", link: "/docs/quickstart" }, + { text: "Walkthrough", link: "/docs/walkthrough" }, ], }, { text: "Guides", items: [ - { text: "Authentication", link: "/guides/authentication" }, - { text: "Hackathon", link: "/guides/hackathon" }, - { text: "Agent", link: "/guides/agent" }, + { text: "Authentication", link: "/docs/guides/authentication" }, + { text: "Hackathon", link: "/docs/guides/hackathon" }, + { text: "Agent", link: "/docs/guides/agent" }, ], }, { text: "Frameworks", items: [ - { text: "React", link: "/components/react" }, - { text: "Vue", link: "/components/vue" }, + { text: "React", link: "/docs/components/react" }, + { text: "Vue", link: "/docs/components/vue" }, ], }, { text: "Components", items: [ - { text: "Gumnut Data", link: "/components/gumnut-data" }, - { text: "Gumnut Focus", link: "/components/gumnut-focus" }, - { text: "Gumnut Status", link: "/components/gumnut-status" }, - { text: "Gumnut Text", link: "/components/gumnut-text" }, + { text: "Gumnut Data", link: "/docs/components/gumnut-data" }, + { text: "Gumnut Focus", link: "/docs/components/gumnut-focus" }, + { text: "Gumnut Status", link: "/docs/components/gumnut-status" }, + { text: "Gumnut Text", link: "/docs/components/gumnut-text" }, ], }, ], @@ -65,20 +102,25 @@ export default defineConfig({ { text: "API Documentation", items: [ - { text: "Gumnut API", link: "/api-reference/gumnut-api" }, - { text: "Gumnut Doc", link: "/api-reference/gumnut-doc" }, - { text: "Gumnut Node", link: "/api-reference/gumnut-node" }, + { text: "Gumnut API", link: "/docs/api-reference/gumnut-api" }, + { text: "Gumnut Doc", link: "/docs/api-reference/gumnut-doc" }, + { text: "Gumnut Node", link: "/docs/api-reference/gumnut-node" }, ], }, ], }, ], - footer: { - copyright: "Copyright © 2025 Gumnut Dev Pty Ltd", + }, + markdown: { + config: (md) => { + md.use(implicitFigures, { + figcaption: true, + copyAttrs: "^class$", + }); }, }, head: [ - ["link", { rel: "icon", href: "/logo/favicon.ico" }], + ["link", { rel: "icon", href: "/images/favicon.ico" }], [ "script", { @@ -88,4 +130,48 @@ export default defineConfig({ }, ], ], + vite: { + css: { + devSourcemap: true, + }, + }, + async transformPageData(pageData) { + if (pageData.relativePath.startsWith("articles/")) { + if (pageData.frontmatter.image) { + pageData.frontmatter.class = "has-header-image"; + } + } + + if (pageData.relativePath.startsWith("case-studies/")) { + if (pageData.frontmatter.image) { + pageData.frontmatter.class = "has-header-image"; + } + } + }, + async buildEnd() { + // Create content loader for blog posts for RSS feed + const postsLoader = createContentLoader("articles/*.md", { + includeSrc: true, + render: true, + excerpt: true, + transform(rawData) { + return rawData + .sort((a, b) => { + const dateA = parseDate(a.frontmatter.date); + const dateB = parseDate(b.frontmatter.date); + return dateB.getTime() - dateA.getTime(); + }) + .map((page) => { + return { + url: page.url, + frontmatter: page.frontmatter, + excerpt: page.excerpt, + }; + }); + }, + }); + + const posts = await postsLoader.load(); + await getFeed(posts); + }, }); diff --git a/docs/.vitepress/getFeed.ts b/docs/.vitepress/getFeed.ts new file mode 100644 index 0000000..0c62473 --- /dev/null +++ b/docs/.vitepress/getFeed.ts @@ -0,0 +1,63 @@ +import { Feed } from "feed"; +import { fileURLToPath } from "url"; +import { dirname, resolve } from "path"; +import fs from "fs"; + +// Define the post type to match VitePress ContentData +interface Post { + url: string; + frontmatter: Record; +} + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +export async function getFeed(posts: Post[]) { + const feed = new Feed({ + title: "The Collaborative Textbox", + description: "Building Gumnut - the modern textbox", + id: "https://blog.gumnut.dev/", + link: "https://blog.gumnut.dev/", + language: "en", + image: "https://blog.gumnut.dev/logo/dark.svg", + favicon: "https://blog.gumnut.dev/logo/dark.svg", + copyright: `Copyright © ${new Date().getFullYear()} Gumnut`, + updated: new Date(), + generator: "VitePress", + feedLinks: { + rss2: "https://blog.gumnut.dev/rss.xml", + }, + }); + + // Add posts to feed + for (const post of posts) { + feed.addItem({ + title: post.frontmatter.title || "Untitled", + id: post.url, + link: `https://blog.gumnut.dev${post.url}`, + description: post.frontmatter.description || "", + content: post.frontmatter.description || "", + author: [ + { + name: post.frontmatter.author || "Gumnut Team", + }, + ], + date: new Date(post.frontmatter.date || Date.now()), + }); + } + + // Ensure the dist directory exists + const distDir = resolve(__dirname, "dist"); + if (!fs.existsSync(distDir)) { + fs.mkdirSync(distDir, { recursive: true }); + } + + // Write feed to file + fs.writeFileSync(resolve(distDir, "rss.xml"), feed.rss2()); + + // Write posts data to a JSON file for the Vue component + fs.writeFileSync( + resolve(distDir, "posts.json"), + JSON.stringify(posts, null, 2) + ); +} diff --git a/docs/.vitepress/posts.data.mts b/docs/.vitepress/posts.data.mts new file mode 100644 index 0000000..1446590 --- /dev/null +++ b/docs/.vitepress/posts.data.mts @@ -0,0 +1,19 @@ +import { createContentLoader } from "vitepress"; + +export default createContentLoader("articles/**/*.md", { + excerpt: true, + transform(raw) { + return raw + .map(({ url, frontmatter, excerpt }) => ({ + title: frontmatter.title, + url, + excerpt, + date: frontmatter.date, + tags: frontmatter.tags || [], + author: frontmatter.author, + description: frontmatter.description, + image: frontmatter.image, + })) + .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); + }, +}); diff --git a/docs/.vitepress/theme/MyLayout.vue b/docs/.vitepress/theme/MyLayout.vue new file mode 100644 index 0000000..2da2b78 --- /dev/null +++ b/docs/.vitepress/theme/MyLayout.vue @@ -0,0 +1,34 @@ + + + \ No newline at end of file diff --git a/docs/.vitepress/theme/components/BlogHeader.vue b/docs/.vitepress/theme/components/BlogHeader.vue new file mode 100644 index 0000000..7f560ee --- /dev/null +++ b/docs/.vitepress/theme/components/BlogHeader.vue @@ -0,0 +1,39 @@ + + + + + \ No newline at end of file diff --git a/docs/.vitepress/theme/components/BlogPosts.vue b/docs/.vitepress/theme/components/BlogPosts.vue new file mode 100644 index 0000000..19d5317 --- /dev/null +++ b/docs/.vitepress/theme/components/BlogPosts.vue @@ -0,0 +1,215 @@ + + + + + \ No newline at end of file diff --git a/docs/.vitepress/theme/components/CaseStudies.vue b/docs/.vitepress/theme/components/CaseStudies.vue new file mode 100644 index 0000000..24e0562 --- /dev/null +++ b/docs/.vitepress/theme/components/CaseStudies.vue @@ -0,0 +1,204 @@ + + + + + \ No newline at end of file diff --git a/docs/.vitepress/theme/components/CaseStudiesSection.vue b/docs/.vitepress/theme/components/CaseStudiesSection.vue new file mode 100644 index 0000000..2735536 --- /dev/null +++ b/docs/.vitepress/theme/components/CaseStudiesSection.vue @@ -0,0 +1,121 @@ + + + \ No newline at end of file diff --git a/docs/.vitepress/theme/components/DemoSection.vue b/docs/.vitepress/theme/components/DemoSection.vue new file mode 100644 index 0000000..870545f --- /dev/null +++ b/docs/.vitepress/theme/components/DemoSection.vue @@ -0,0 +1,226 @@ + + + + + \ No newline at end of file diff --git a/docs/.vitepress/theme/components/FeaturesDetailSection.vue b/docs/.vitepress/theme/components/FeaturesDetailSection.vue new file mode 100644 index 0000000..8765490 --- /dev/null +++ b/docs/.vitepress/theme/components/FeaturesDetailSection.vue @@ -0,0 +1,359 @@ + + + \ No newline at end of file diff --git a/docs/.vitepress/theme/components/FeaturesOverviewSection.vue b/docs/.vitepress/theme/components/FeaturesOverviewSection.vue new file mode 100644 index 0000000..30c34bc --- /dev/null +++ b/docs/.vitepress/theme/components/FeaturesOverviewSection.vue @@ -0,0 +1,127 @@ + + + \ No newline at end of file diff --git a/docs/.vitepress/theme/components/RecentPosts.vue b/docs/.vitepress/theme/components/RecentPosts.vue new file mode 100644 index 0000000..5a1665c --- /dev/null +++ b/docs/.vitepress/theme/components/RecentPosts.vue @@ -0,0 +1,110 @@ + + + + + \ No newline at end of file diff --git a/docs/.vitepress/theme/components/SiteFooter.vue b/docs/.vitepress/theme/components/SiteFooter.vue new file mode 100644 index 0000000..a770823 --- /dev/null +++ b/docs/.vitepress/theme/components/SiteFooter.vue @@ -0,0 +1,138 @@ + + + \ No newline at end of file diff --git a/docs/.vitepress/theme/components/SocialProofSection.vue b/docs/.vitepress/theme/components/SocialProofSection.vue new file mode 100644 index 0000000..f67a421 --- /dev/null +++ b/docs/.vitepress/theme/components/SocialProofSection.vue @@ -0,0 +1,128 @@ + + + \ No newline at end of file diff --git a/docs/.vitepress/theme/custom.css b/docs/.vitepress/theme/custom.css index 16a7749..2a55db6 100644 --- a/docs/.vitepress/theme/custom.css +++ b/docs/.vitepress/theme/custom.css @@ -19,12 +19,6 @@ /* Additional tip color */ --color-tip: #22863a; - - --vp-c-brand: #646cff; - --vp-c-brand-light: #747bff; - --vp-c-brand-lighter: #9499ff; - --vp-c-brand-dark: #535bf2; - --vp-c-brand-darker: #454ce1; } .VPButton.brand { @@ -44,3 +38,103 @@ margin: 0.5rem 0; font-weight: 400; } + +/* Consistent page layout for pricing and questions pages */ +.pricing-page, +.questions-page { + max-width: 1200px !important; + margin: 0 auto !important; + padding: 1rem !important; +} + +/* Override VitePress default content width constraints */ +.VPDoc .content-container { + max-width: none !important; +} + +.VPDoc .content { + max-width: none !important; +} + +/* Force wider layout for blog and case studies pages */ +.blog-posts, +.case-studies { + max-width: 1600px !important; + width: 100% !important; +} + +/* Align headings with content for pricing and questions pages */ +.VPDoc .content h1 { + margin-left: 1rem !important; + padding-left: 0 !important; +} + +/* Align h1 headings with content for blog and case studies pages */ +.blog-posts h1, +.case-studies h1, +.VPDoc .content h1 { + margin-left: 0 !important; + padding-left: 0 !important; +} + +/* Hide external link arrows */ +.VPNavBarMenuLink .icon, +.VPNavBarMenuLink .external-link-icon, +.VPNavBarMenuLink::after { + display: none !important; +} + +/* Style the Book a demo navigation link */ +.VPNav + .VPNavBar + .container + .content + .VPNavBarMenu + .VPNavBarMenuLink[href*="calendly.com"] { + background: var(--vp-c-brand-1); + color: white !important; + border-radius: 4px; + padding: 4px 8px; + margin-left: 8px; + height: auto !important; + line-height: 1.2; + display: inline-block; + align-self: center; + margin-top: auto; + margin-bottom: auto; +} + +.VPNav + .VPNavBar + .container + .content + .VPNavBarMenu + .VPNavBarMenuLink[href*="calendly.com"]:hover { + background: var(--vp-c-brand-2); + color: white !important; +} + +/* Direct tagline styling */ +.tagline { + font-size: 1.2rem !important; +} + +/* Make tip block links more visible */ +.custom-block.tip a { + color: #4ade80 !important; +} + +.custom-block.tip a:hover { + color: #22c55e !important; +} + +/* Navbar Logo Styling */ +.VPNav .VPNavBar .container .title .logo { + height: 32px !important; + width: auto !important; +} + +.VPNav .VPNavBar .container .title .logo img { + height: 32px !important; + width: auto !important; +} diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts index c495bc1..a7752dc 100644 --- a/docs/.vitepress/theme/index.ts +++ b/docs/.vitepress/theme/index.ts @@ -1,4 +1,38 @@ import DefaultTheme from "vitepress/theme"; +import "./style.css"; import "./custom.css"; +// @ts-ignore +import MyLayout from "./MyLayout.vue"; +// @ts-ignore +import BlogPosts from "./components/BlogPosts.vue"; +// @ts-ignore +import CaseStudies from "./components/CaseStudies.vue"; +// @ts-ignore +import BlogHeader from "./components/BlogHeader.vue"; +import { inBrowser } from "vitepress"; -export default DefaultTheme; +export default { + extends: DefaultTheme, + Layout: MyLayout as any, + enhanceApp({ app }: { app: any }) { + // Register the BlogPosts component + app.component("BlogPosts", BlogPosts); + // Register the CaseStudies component + app.component("CaseStudies", CaseStudies); + // Register the BlogHeader component + app.component("BlogHeader", BlogHeader); + + if (inBrowser) { + import("../usePostHog").then(({ usePostHog }) => { + const { posthog } = usePostHog(); + + // Track page views + window.addEventListener("load", () => { + posthog.capture("$pageview", { + $current_url: window.location.href, + }); + }); + }); + } + }, +}; diff --git a/docs/.vitepress/theme/posts.data.ts b/docs/.vitepress/theme/posts.data.ts new file mode 100644 index 0000000..028a7b6 --- /dev/null +++ b/docs/.vitepress/theme/posts.data.ts @@ -0,0 +1,17 @@ +import { createContentLoader } from "vitepress"; + +// This file is used by the theme to provide post data to components +export default { + watch: ["articles/*.md"], + load(watchedFiles) { + return watchedFiles.map((file) => { + // This is a simplified version - in a real implementation, + // you would parse the frontmatter and content here + return { + url: file.replace(/^articles\//, "/").replace(/\.md$/, ".html"), + frontmatter: {}, + excerpt: "", + }; + }); + }, +}; diff --git a/docs/.vitepress/theme/style.css b/docs/.vitepress/theme/style.css new file mode 100644 index 0000000..07f7e30 --- /dev/null +++ b/docs/.vitepress/theme/style.css @@ -0,0 +1,346 @@ +/** + * Customize default theme styling by overriding CSS variables: + * https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css + */ + +/** + * Colors + * + * Each colors have exact same color scale system with 3 levels of solid + * colors with different brightness, and 1 soft color. + * + * - `XXX-1`: The most solid color used mainly for colored text. It must + * satisfy the contrast ratio against when used on top of `XXX-soft`. + * + * - `XXX-2`: The color used mainly for hover state of the button. + * + * - `XXX-3`: The color for solid background, such as bg color of the button. + * It must satisfy the contrast ratio with pure white (#ffffff) text on + * top of it. + * + * - `XXX-soft`: The color used for subtle background such as custom container + * or badges. It must satisfy the contrast ratio when putting `XXX-1` colors + * on top of it. + * + * The soft color must be semi transparent alpha channel. This is crucial + * because it allows adding multiple "soft" colors on top of each other + * to create a accent, such as when having inline code block inside + * custom containers. + * + * - `default`: The color used purely for subtle indication without any + * special meanings attached to it such as bg color for menu hover state. + * + * - `brand`: Used for primary brand colors, such as link text, button with + * brand theme, etc. + * + * - `tip`: Used to indicate useful information. The default theme uses the + * brand color for this by default. + * + * - `warning`: Used to indicate warning to the users. Used in custom + * container, badges, etc. + * + * - `danger`: Used to show error, or dangerous message to the users. Used + * in custom container, badges, etc. + * -------------------------------------------------------------------------- */ + +:root { + --vp-c-default-1: var(--vp-c-gray-1); + --vp-c-default-2: var(--vp-c-gray-2); + --vp-c-default-3: var(--vp-c-gray-3); + --vp-c-default-soft: var(--vp-c-gray-soft); + + --vp-c-brand-1: #da509a; + --vp-c-brand-2: #c0488a; + --vp-c-brand-3: #a63f75; + --vp-c-brand-soft: rgba(218, 80, 154, 0.15); + + --vp-c-tip-1: var(--vp-c-green-1); + --vp-c-tip-2: var(--vp-c-green-2); + --vp-c-tip-3: var(--vp-c-green-3); + --vp-c-tip-soft: var(--vp-c-green-soft); + + --vp-c-warning-1: var(--vp-c-yellow-1); + --vp-c-warning-2: var(--vp-c-yellow-2); + --vp-c-warning-3: var(--vp-c-yellow-3); + --vp-c-warning-soft: var(--vp-c-yellow-soft); + + --vp-c-danger-1: var(--vp-c-red-1); + --vp-c-danger-2: var(--vp-c-red-2); + --vp-c-danger-3: var(--vp-c-red-3); + --vp-c-danger-soft: var(--vp-c-red-soft); + + /* Make text-2 brighter */ + --vp-c-text-2: #9ca3af; +} + +/* Light mode */ +:root { + --vp-c-text-2: #6b7280; +} + +/* Dark mode */ +.dark { + --vp-c-text-2: #9ca3af; +} + +/** + * Component: Button + * -------------------------------------------------------------------------- */ + +:root { + --vp-button-brand-border: transparent; + --vp-button-brand-text: var(--vp-c-white); + --vp-button-brand-bg: #da509a; + --vp-button-brand-hover-border: transparent; + --vp-button-brand-hover-text: var(--vp-c-white); + --vp-button-brand-hover-bg: #c0488a; + --vp-button-brand-active-border: transparent; + --vp-button-brand-active-text: var(--vp-c-white); + --vp-button-brand-active-bg: #da509a; +} + +/** + * Component: Home + * -------------------------------------------------------------------------- */ + +:root { + --vp-home-hero-name-color: transparent; + --vp-home-hero-name-background: -webkit-linear-gradient( + 120deg, + var(--vp-c-brand-1) 30%, + #ffe4e1 + ); + + --vp-home-hero-image-background-image: linear-gradient( + -45deg, + rgba(218, 80, 154, 0.15) 50%, + rgba(192, 72, 138, 0.15) 50% + ); + --vp-home-hero-image-filter: blur(44px); +} + +@media (min-width: 640px) { + :root { + --vp-home-hero-image-filter: blur(56px); + } +} + +@media (min-width: 960px) { + :root { + --vp-home-hero-image-filter: blur(68px); + } +} + +/** + * Component: Custom Block + * -------------------------------------------------------------------------- */ + +:root { + --vp-custom-block-tip-border: transparent; + --vp-custom-block-tip-text: var(--vp-c-text-1); + --vp-custom-block-tip-bg: var(--vp-c-tip-3); + --vp-custom-block-tip-code-bg: var(--vp-c-tip-soft); +} + +/** + * Component: Algolia + * -------------------------------------------------------------------------- */ + +.DocSearch { + --docsearch-primary-color: var(--vp-c-brand-1) !important; +} + +/** + * Component: Figures + * -------------------------------------------------------------------------- */ + +figure img { + border: 1px solid var(--vp-c-divider); + border-radius: 4px; +} + +figure figcaption { + text-align: center; + font-size: 0.9em; + color: var(--vp-c-text-2); + margin-top: 0.5em; +} + +.has-header-image .vp-doc { + padding-top: 0; +} + +.has-header-image .vp-doc > :first-child { + margin-top: 0; +} + +.has-header-image .vp-doc > :first-child img { + width: 100%; + max-height: 400px; + object-fit: cover; + margin-bottom: 2rem; +} + +/** + * Component: Pricing Page + * -------------------------------------------------------------------------- */ + +.pricing-grid { + display: grid; + grid-template-columns: repeat(3, minmax(280px, 1fr)); + gap: 2rem; + margin: 2rem 0; + align-items: stretch; + width: 100%; +} + +.pricing-card { + border: 1px solid var(--vp-c-divider); + border-radius: 12px; + padding: 2rem; + background: var(--vp-c-bg); + position: relative; + display: grid; + grid-template-rows: auto 1fr auto; + height: 100%; + width: 100%; + min-width: 0; + transition: box-shadow 0.2s ease; +} + +.tier-label { + position: absolute; + top: -12px; + left: 50%; + transform: translateX(-50%); + background: var(--vp-c-brand-1); + color: white; + padding: 4px 12px; + border-radius: 12px; + font-size: 0.8rem; + font-weight: 600; + z-index: 1; +} + +.pricing-card.featured { + border-color: var(--vp-c-brand-1); + box-shadow: 0 4px 12px rgba(218, 80, 154, 0.15); +} + +.pricing-header { + text-align: center; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.25rem; + margin-bottom: 2rem; +} + +.pricing-header h3 { + margin: 0; + margin-bottom: -0.25rem; + font-size: 1.5rem; + font-weight: 600; + color: var(--vp-c-text-1); +} + +.tier-description { + margin: 0; + margin-top: -0.25rem; +} + +.pricing-features { + flex: 1; + margin-bottom: 2rem; +} + +.pricing-features ul { + list-style: none; + padding: 0; + margin: 0; +} + +.pricing-features li { + padding: 0.5rem 0; +} + +.pricing-features li::before { + content: "✓"; + color: var(--vp-c-brand-1); + font-weight: bold; + margin-right: 0.5rem; +} + +.pricing-icon { + display: flex; + justify-content: center; + align-items: center; +} + +.pricing-icon img { + width: 96px; + height: 96px; + object-fit: contain; +} + +.pricing-action { + margin-top: auto; + text-align: center; +} + +.pricing-action .btn { + display: inline-block; + padding: 0.75rem 2rem; + border-radius: 6px; + text-decoration: none; + font-weight: 500; +} + +.pricing-action .btn-primary { + background-color: var(--vp-c-brand-1); + color: white; + border: 1px solid var(--vp-c-brand-1); +} + +.pricing-action .btn-outline { + background-color: transparent; + color: var(--vp-c-brand-1); + border: 1px solid var(--vp-c-brand-1); +} + +.price { + margin: 0; + text-align: center; +} + +.price .amount { + font-size: 3rem; + font-weight: 700; + color: var(--vp-c-brand-1); +} + +.price .period { + font-size: 1rem; + color: var(--vp-c-text-2); +} + +/* Responsive design for pricing grid */ +@media (max-width: 1024px) { + .pricing-grid { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 768px) { + .pricing-grid { + grid-template-columns: 1fr; + } + + .pricing-card.featured { + transform: none; + } + + .amount { + font-size: 2.5rem; + } +} diff --git a/docs/.vitepress/theme/tsconfig.json b/docs/.vitepress/theme/tsconfig.json new file mode 100644 index 0000000..0f77465 --- /dev/null +++ b/docs/.vitepress/theme/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "types": ["vitepress/client", "vue"], + "strict": true, + "skipLibCheck": true + } +} diff --git a/docs/.vitepress/usePostHog.ts b/docs/.vitepress/usePostHog.ts new file mode 100644 index 0000000..af98224 --- /dev/null +++ b/docs/.vitepress/usePostHog.ts @@ -0,0 +1,11 @@ +import posthog from "posthog-js"; + +export function usePostHog() { + posthog.init("phc_rUFVVeEW91VmDXwiB5gp93dHBljkFjC16I3WvZcw8ub", { + api_host: "https://us.i.posthog.com", + }); + + return { + posthog, + }; +} diff --git a/docs/articles/article-1.md b/docs/articles/article-1.md new file mode 100644 index 0000000..515499f --- /dev/null +++ b/docs/articles/article-1.md @@ -0,0 +1,14 @@ +--- +title: Hello World +date: 2025-04-09 +description: Welcome to the Gumnut blog +sidebar: false +prev: false +next: false +--- + +# Welcome to the Gumnut Blog + +We created this blog to document some hopefully useful tangents we've come across while building Gumnut. + +Maybe it'll stay like this, but this works for now. diff --git a/docs/articles/future-of-work-ai.md b/docs/articles/future-of-work-ai.md new file mode 100644 index 0000000..22f947e --- /dev/null +++ b/docs/articles/future-of-work-ai.md @@ -0,0 +1,54 @@ +--- +title: Beyond the helpdesk - AI human collaboration frontiers +description: Solving the interface of human/ai collaboration in modern applications +date: 2025-06-13 +sidebar: false +prev: false +next: false +--- + +# Beyond the helpdesk - AI human collaboration frontiers + +I recently read [this article in Enterprise Times](https://www.enterprisetimes.co.uk/2025/06/12/human-workers-and-ai-agents-collaboration-the-key-says-zahra-bahrololoumi-at-salesforce-world-tour-in-london/) about how human/ai collaboration is key. I've been thinking about this topic for a while, and it's really cool to see how enterprises are actively working on this, to make the user experience and productivity amazing, and AI can do a lot to improve that. + +## 🎤 World tour 💃 + +The article does focus on the [Salesforce World Tour](https://www.salesforce.com/uk/events/london/) which was recently in London, and I didn't know that it was a rock-concert-like _actual_ world tour. But it definitely is, they're even coming to Australia in a [couple of weeks](https://www.salesforce.com/au/events/) 🤘 + +Salesforce's impressive statistic is Agentforce-resolving 84% of customer support queries with minimal human intervention-is a testament to the strides made in AI-driven automation. Only 2% required human intervention. Honestly these numbners are really impressive. But it represents just the initial phase of AI's potential in the enterprise. + +The transition to AI-first operations is not just a trend (lol, genius). According to the [Digital Enterprise 2025](https://community.nasscom.in/communities/digital-transformation/digital-enterprise-2025-advancing-ai-first-enterprise) report by Nasscom and Avasant, 27% of companies have AI in production or at scale, with another 31% at the proof-of-concept stage. Furthermore, 74% of enterprises expect AI spending to increase in 2025, underscoring the growing emphasis on AI integration in enterprises. + +The thing that I think is missing from these kinds of presentations/reports (sorry enterprise, I promise I don't hate you), is _what does that actually look like tomorrow?_ + +Is it really just answering emails and customer support requests? I don't think anyone thinks that, but what does the future of human and AI collaboration look and feel like moving forward? + +This is where the startup space can provide a bit of a window into that future. + +## 🚀 The future + +I think a good company to look at is [Relevance AI](https://relevanceai.com/) in this space. + +Relevance lets companies create a whole "AI workforce" (literally), each agent can spawn a series of sub-agents (in their new product Workforce). You can construct a whole network of agents to perform a tasks. + +![Relevance AI workforce](https://cdn.prod.website-files.com/636cad09a1159553a45e8ba1/67c105469bd221272da70758_workforce-builder-hero.png) + +The reason you do this, is, just like humans, agents are much more effective when you can absract larger tasks into smaller, manageble pieces, and have a dedicated agent to just do that thing. + +Most of the applications I've seen so far are in sales research, answering emails/queries. But it's actually configurable to the extent that you can imaging _any_ application involving AI to be done autonomously. Including any external tools callable via MCP, by agents inside Relevance. + +## 🤸 The human part + +But there thing that's missing in that, is _where is the human interaction?_ What does Human/AI interaction look like? + +Right now, it's in interfaces like _Cursor_ - you have a chat window, you type and it does stuff. + +![Cursor interface (I did write this article in Cursor)](/public/images/cursor-interface.png) + +Companies like [Gumnut](https://gumnut.dev) (disclosure: my company, but you are on my blog...) are interesting in that - we have a real-time collaboration platforms where AI can type alongside you as a co-pilot. Think Google Docs, but the colleagues you're typing with are AI agents typing alongside you. + +We'll see what [Liveblocks](https://www.linkedin.com/posts/steven-fabre-5510bb38_nothing-meaningful-is-built-alone-that-activity-7338946791880511490-FYQL)) is cooking in this space too in the coming weeks. + +There are lots of _interface_ questions as to the best way to interact with AI agents as you type alongside them. Is it just another Cursor-like chatbot like [TipTap](http://tiptap.dev/product/ai-agent) have? Or something else? + +We'll have a lot more to say on this soon 👀, but if you want to try it yourself (and you're in Sydney), come to our [hackathon](https://gumnut.dev/hack) on July 2nd and see what you can come up with! diff --git a/docs/articles/new-indian-express-ai-adoption.md b/docs/articles/new-indian-express-ai-adoption.md new file mode 100644 index 0000000..c1620c0 --- /dev/null +++ b/docs/articles/new-indian-express-ai-adoption.md @@ -0,0 +1,47 @@ +--- +title: Data on Enterprise AI adoption +description: AI agent adoption in enterprises +date: 2025-06-02 +author: "Owen Brasier" +sidebar: false +prev: false +next: false +--- + +# AI agent adoption in enterprise + +I read an interesting article on the adoption of AI agents in the [New Indian Express](https://www.newindianexpress.com/xplore/2025/May/31/from-experimentation-to-deployment-it-firms-now-focus-on-ai-agents-2), and how they are being adopted in enterprise. + +::: info QUOTE +Currently, 27% of enterprises report having AI agents in production or at scale, while 31% are at the proof-of-concept (PoC) stage, and another 30% plan to initiate PoCs or scale deployments in CY2025. Enterprises are expected to spend 3-4X more on AI agents in CY2025 as a result of synchronous AI systems +::: + +The figures come from [this report](https://community.nasscom.in/communities/digital-transformation/digital-enterprise-2025-advancing-ai-first-enterprise) from Nasscom. The report is quite interesting in that it covers _what is being spent_ on AI by enterprises. + +Here is the graph that illustrates these figures: + +![AI agent adoption CY2024: [source](https://community.nasscom.in/communities/digital-transformation/digital-enterprise-2025-advancing-ai-first-enterprise)](/public/images/ai-agent-deployment-nasscom.png) + +## What it covers + +The article and report certainly highlights the adoption of AI agents increasing across enterprises in 2025. I don't think that comes as a shock to... anyone. It does give an interesting sector breakdown: + +![Sector adoption of AI agents CY2025: [source](https://community.nasscom.in/communities/digital-transformation/digital-enterprise-2025-advancing-ai-first-enterprise)](/public/images/ai-adoption-sector-nasscom.png) + +I'm surprised regualted industries like banking and healthcare feature so highly, honestly. Maybe manufacturing is already so heavily automated that AI doesn't really make that much of a difference to automation processes? + +## What it misses + +What I'd really love to see is _how_ AI agents are being deployed/utilised across sectors. Are enterprises building them in-house? Are they migrating to AI workforces like [Relevance AI](https://relevance.ai/) to automate tasks? Or just using something like [Lyzr](http://lyzr.ai/) to deploy multi-agents systems? + +Or are they just automating processes by using ChatGPT/Gemini/_insert provider here_ to automate writing emails like everyone else? + +Maybe that's a little facetious, the [New Indian Express](https://www.newindianexpress.com/xplore/2025/May/31/from-experimentation-to-deployment-it-firms-now-focus-on-ai-agents-2) does say: + +::: info QUOTE +Leveraging advanced machine learning and cognitive architecture, the agents are equipped with powerful capabilities such as data extraction to transform raw inputs into actionable insights and multimodal functionality to handle diverse data types effectively +::: + +Although helping deal with messy data doesn't sound to me like they're mass replacing data scientists with AI, more like the processes are more streamlined. Though aren't we all doing that? + +I really think that the enterprises to _really_ jump on embedded AI into their systems processes will be able to move fast. The thing is though, is that startups move faster, so I think we're living in a very interesting time. diff --git a/docs/articles/using-ai-to-clone-sites.md b/docs/articles/using-ai-to-clone-sites.md new file mode 100644 index 0000000..1aaa39f --- /dev/null +++ b/docs/articles/using-ai-to-clone-sites.md @@ -0,0 +1,211 @@ +--- +title: Using AI to clone sites +description: How Gumnut uses AI to convert screenshots into interactive web pages +date: 2025-04-15 +sidebar: false +prev: false +next: false +--- + +# Using AI to clone sites + +At [Gumnut](https://gumnut.dev), we have built the modern textbox, helping old-school HTML forms grow up. + +::: info +Gumnut adds real-time collaboration, version control and attribution anywhere. Improving productivity, quality and compliance with data. +::: + +But we wanted to build a tool that can _show_ you what it looks like, to get the vibe of the thing - as we're creating a new type of editor! So rather than having you write code, you could just give it a go. + +So we built our demo page, sorry for the cheesy copy. + +![Demo Site Screenshot](/images/demo-site-screenshot.png) + +## How it works + +Hopefully this is an example of a Good Use of AI™. The workflow should be: + +!["The journey"](/images/workflow-1.png) + +We're using Google's stack so our architecture is a couple of steps over Firebase and friends: + +![Process to create the demo](/images/workflow-2.png) + +## Screenshot to HTML + +Uploading the screenshot is pretty straightforward JS. + +```js +// Generate a unique site ID +const siteId = generateUniqueId(); +const imageURL = await getDataURL(currentFile); + +const functionData = { + email: email, + screenshotURL: imageURL, + siteId: siteId, +}; + +const result = await generateWebsiteFunction(functionData); +// redirect viewer to iframe +window.location.href = `viewer.html?id=${siteId}`; +``` + +So that's pretty straightforward. We pass a URL and siteID to Firebase to process it. So how does `generateWebsiteFunction` work? + +In simple terms, we send the image to Gemini using the `gemini-2.0-flash` model (it's fast and pretty good). To do that, we get a `base64` image and the `content-type`, and then use `@google/generative-ai` library to send to Gemini. + +::: tip +I tried this with Claude 3.7 and it worked fine, but `gemini` was faster so we went with that. On really big sites Claude was a bit painfully slow. +::: + +We get the `content-type` from the image, and the + +```js +const matches = screenshotUrl.match(/^data:([A-Za-z-+/]+);base64,(.+)$/); +if (!matches) { + throw new Error("Invalid data URL format"); +} +contentType = matches[1]; +const base64Data = matches[2]; +imageBuffer = Buffer.from(base64Data, "base64"); +``` + +### Initial prompt + +```js +const initialPrompt = `You are an expert CSS and HTML developer. + +Analyze this screenshot and generate HTML/CSS code that recreates EXACTLY what is shown in the uploaded image.Faithfully recreate the UI shown in the screenshot, treating it as a completely separate design. + +Pay close attention to background color, text color, font size, font family, padding, margin, border, etc.Match the colors, layout, and text content exactly as shown in the screenshot.Make sure the text color inside the textareas is readable, and the text is not too small. + +If you detect any images, replace them with a placeholder, or use an emoji to replace them. There should be no images in the final output. + +Return ONLY the complete HTML with inline CSS. No additional JavaScript. No explanation. No markdown formatting. + +Your response should start with and include everything needed for a standalone page.`; +``` + +It's pretty intense, but it works well enough. You have to treat the AI like a toddler (in basically all circumstances), and just like a toddler it will go rogue given any excuse. + +It can be called by a simple snippet: + +```js +import { GoogleGenerativeAI } from "@google/generative-ai"; +const genAI = new GoogleGenerativeAI(apiKey); +const model = genAI.getGenerativeModel("gemini-2.0-flash"); +const initialResult = await model.generateContent([ + { + inlineData: { + data: base64Screenshot, + mimeType: contentType, + }, + }, + initialPrompt, +]); +``` + +We run it again anyway, since `Gemini` is quite fast, and the improved results make it worth producing it twice. + +```js +const verificationPrompt = `Compare the original screenshot with the HTML implementation I provided. +Identify any visual differences, missing elements, or styling inconsistencies. + +Then generate an improved HTML version that better matches the original screenshot. +Focus on fixing: +1. Missing or incorrect text content +2. Color mismatches (background, text, borders) +3. Layout and spacing issues +4. Font sizes and styles +5. Missing UI elements +Pay specific attention to colors, and make sure writing color has good contrast with the background color. Replace all images with an emoji or a placeholder. + +Return ONLY the improved HTML. No explanation needed.`; +const verificationResult = await model.generateContent([ + { + inlineData: { + data: base64Screenshot, + mimeType: contentType, + }, + }, + "Here is the first HTML attempt:", + generatedHtml, + verificationPrompt, +]); +``` + +Even with this extra step, sometimes the output is far off the original, but it's still good enough for most things we throw at it. + +## Upgrading the input to use Gumnut + +Once we have the HTML downloaded, we need to upgrade all the `` and `