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
4 changes: 3 additions & 1 deletion .github/workflows/publish-npm-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ on:
workflow_dispatch:
inputs:
package:
description: "Package to publish (react-ui, react-headless, react-lang, react-email or openui-cli)"
description: "Package to publish"
required: true
type: choice
options:
Expand All @@ -17,6 +17,8 @@ on:
- react-lang
- react-email
- openui-cli
- svelte-lang
- vue-lang

jobs:
publish:
Expand Down
1 change: 1 addition & 0 deletions examples/vue-chat/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
OPENAI_API_KEY=sk-...
3 changes: 3 additions & 0 deletions examples/vue-chat/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.nuxt/
.output/
node_modules/
3 changes: 3 additions & 0 deletions examples/vue-chat/app.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<template>
<NuxtPage />
</template>
1 change: 1 addition & 0 deletions examples/vue-chat/assets/app.css
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@import "tailwindcss";
73 changes: 73 additions & 0 deletions examples/vue-chat/components/AssistantMessage.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<script setup lang="ts">
import { computed } from "vue";
import { Renderer, type ActionEvent, type Library } from "@openuidev/vue-lang";

const props = defineProps<{
parts: any[];
library: Library;
isStreaming: boolean;
onAction: (event: ActionEvent) => void;
}>();

const textContent = computed(() =>
props.parts
.filter((p: any) => p.type === "text")
.map((p: any) => p.text)
.join(""),
);

const toolParts = computed(() =>
props.parts.filter((p: any) => p.type?.startsWith("tool-") || p.type === "dynamic-tool"),
);

function getToolName(part: any): string {
if (part.type === "dynamic-tool" && "toolName" in part) return part.toolName;
return part.type?.replace(/^tool-/, "") ?? "";
}
</script>

<template>
<div class="flex gap-3">
<div
class="shrink-0 w-7 h-7 rounded-full bg-gradient-to-br from-violet-500 to-indigo-600 flex items-center justify-center"
>
<span class="text-[10px] font-bold text-white">AI</span>
</div>
<div class="flex-1 min-w-0 space-y-2">
<div
v-for="(tp, i) in toolParts"
:key="i"
class="flex items-center gap-2 text-xs text-zinc-500 dark:text-zinc-400"
>
<template v-if="(tp as any).state === 'output-available'">
<svg
class="w-3.5 h-3.5 text-green-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2.5"
d="M5 13l4 4L19 7"
/>
</svg>
</template>
<template v-else>
<div
class="w-3.5 h-3.5 border-2 border-zinc-300 dark:border-zinc-600 border-t-transparent rounded-full animate-spin"
></div>
</template>
<span class="font-medium">{{ getToolName(tp) }}</span>
</div>
<Renderer
v-if="textContent"
:response="textContent"
:library="library"
:is-streaming="isStreaming"
:on-action="onAction"
/>
</div>
</div>
</template>
10 changes: 10 additions & 0 deletions examples/vue-chat/components/ChatHeader.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<template>
<header
class="shrink-0 border-b border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 px-6 py-4"
>
<h1 class="text-lg font-semibold text-zinc-900 dark:text-zinc-100">OpenUI Vue Chat</h1>
<p class="text-xs text-zinc-500 dark:text-zinc-400">
Powered by @openuidev/vue-lang &amp; Vercel AI SDK
</p>
</header>
</template>
59 changes: 59 additions & 0 deletions examples/vue-chat/components/ChatInput.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<script setup lang="ts">
import { ref } from "vue";

const props = defineProps<{
isLoading: boolean;
}>();

const emit = defineEmits<{
submit: [text: string];
stop: [];
}>();

const input = ref("");

function handleSubmit() {
const text = input.value.trim();
if (!text || props.isLoading) return;
input.value = "";
emit("submit", text);
}

function handleKeydown(e: KeyboardEvent) {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSubmit();
}
}
</script>

<template>
<div class="shrink-0 border-t border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 p-4">
<form class="max-w-3xl mx-auto flex gap-2" @submit.prevent="handleSubmit">
<input
v-model="input"
type="text"
placeholder="Type a message..."
:disabled="isLoading"
class="flex-1 rounded-xl border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 px-4 py-2.5 text-sm text-zinc-900 dark:text-zinc-100 placeholder-zinc-400 focus:outline-none focus:ring-2 focus:ring-violet-500 focus:border-transparent disabled:opacity-50"
@keydown="handleKeydown"
/>
<button
v-if="isLoading"
type="button"
class="rounded-xl bg-zinc-200 dark:bg-zinc-700 px-5 py-2.5 text-sm font-medium text-zinc-700 dark:text-zinc-300 hover:bg-zinc-300 dark:hover:bg-zinc-600 transition-colors cursor-pointer"
@click="$emit('stop')"
>
Stop
</button>
<button
v-else
type="submit"
:disabled="!input.trim()"
class="rounded-xl bg-violet-600 px-5 py-2.5 text-sm font-medium text-white hover:bg-violet-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer"
>
Send
</button>
</form>
</div>
</template>
16 changes: 16 additions & 0 deletions examples/vue-chat/components/LoadingIndicator.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<template>
<div class="flex gap-3">
<div
class="shrink-0 w-7 h-7 rounded-full bg-gradient-to-br from-violet-500 to-indigo-600 flex items-center justify-center"
>
<span class="text-[10px] font-bold text-white">AI</span>
</div>
<div class="flex items-center gap-1.5 py-2">
<div class="w-1.5 h-1.5 bg-zinc-400 rounded-full animate-bounce"></div>
<div
class="w-1.5 h-1.5 bg-zinc-400 rounded-full animate-bounce [animation-delay:0.15s]"
></div>
<div class="w-1.5 h-1.5 bg-zinc-400 rounded-full animate-bounce [animation-delay:0.3s]"></div>
</div>
</div>
</template>
15 changes: 15 additions & 0 deletions examples/vue-chat/components/UserMessage.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<script setup lang="ts">
defineProps<{ parts: any[] }>();
</script>

<template>
<div class="flex justify-end">
<div
class="bg-zinc-900 dark:bg-zinc-100 text-white dark:text-zinc-900 rounded-2xl rounded-br-sm px-4 py-2.5 max-w-[80%]"
>
<template v-for="(part, i) in parts" :key="i">
<p v-if="part.type === 'text'" class="text-sm">{{ part.text }}</p>
</template>
</div>
</div>
</template>
27 changes: 27 additions & 0 deletions examples/vue-chat/components/WelcomeScreen.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<script setup lang="ts">
defineProps<{ starters: string[] }>();
const emit = defineEmits<{ send: [text: string] }>();
</script>

<template>
<div class="flex flex-col items-center justify-center h-full px-4">
<div class="text-center mb-8">
<h2 class="text-2xl font-semibold text-zinc-900 dark:text-zinc-100 mb-2">
Welcome to OpenUI Chat
</h2>
<p class="text-zinc-500 dark:text-zinc-400">
Ask anything — responses are rendered as structured UI components.
</p>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 max-w-lg w-full">
<button
v-for="starter in starters"
:key="starter"
class="text-left rounded-xl border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 px-4 py-3 text-sm text-zinc-700 dark:text-zinc-300 hover:bg-zinc-50 dark:hover:bg-zinc-800 transition-colors cursor-pointer"
@click="emit('send', starter)"
>
{{ starter }}
</button>
</div>
</div>
</template>
21 changes: 21 additions & 0 deletions examples/vue-chat/components/openui/Button.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<script setup lang="ts">
import { useTriggerAction } from "@openuidev/vue-lang";

const { props } = defineProps<{ props: { label?: string; action?: string } }>();
const triggerAction = useTriggerAction();
</script>

<template>
<button
class="inline-flex items-center rounded-lg border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 px-3 py-1.5 text-sm font-medium text-zinc-700 dark:text-zinc-300 hover:bg-zinc-50 dark:hover:bg-zinc-700 transition-colors cursor-pointer"
@click="
triggerAction(
props.label ?? 'Click',
undefined,
props.action ? { type: props.action } : undefined,
)
"
>
{{ props.label ?? "Button" }}
</button>
</template>
19 changes: 19 additions & 0 deletions examples/vue-chat/components/openui/Card.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<script setup lang="ts">
import { useRenderNode } from "@openuidev/vue-lang";

const { props } = defineProps<{ props: { title?: string; children?: unknown } }>();
const renderNode = useRenderNode();
</script>

<template>
<div
class="rounded-xl border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 p-4 shadow-sm"
>
<h3 v-if="props.title" class="text-base font-semibold text-zinc-900 dark:text-zinc-100 mb-2">
{{ props.title }}
</h3>
<div v-if="props.children" class="space-y-2">
<component :is="() => renderNode(props.children)" />
</div>
</div>
</template>
Loading
Loading