This commit is contained in:
zramsay 2025-01-09 12:26:11 -05:00
commit b44bca96ac
30 changed files with 9306 additions and 0 deletions

5
next-env.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

18
next.config.mjs Normal file
View File

@ -0,0 +1,18 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
webpack: (config) => {
config.resolve.fallback = {
...config.resolve.fallback,
fs: false,
os: false,
path: false,
crypto: false,
}
return config
},
env: {
GOOGLE_API_KEY: process.env.GOOGLE_API_KEY,
},
}
export default nextConfig

7359
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

38
package.json Normal file
View File

@ -0,0 +1,38 @@
{
"name": "wildlife",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@fal-ai/client": "^1.2.1",
"@google-cloud/vision": "^4.3.2",
"@google/generative-ai": "^0.21.0",
"@solana/spl-token": "^0.3.8",
"@solana/web3.js": "^1.78.4",
"exif-reader": "^2.0.1",
"form-data": "^4.0.1",
"lucide-react": "^0.469.0",
"next": "^15.1.4",
"openai": "^4.77.0",
"react": "^18",
"react-dom": "^18",
"yaml": "^2.7.0"
},
"devDependencies": {
"@types/exif-js": "^2.3.1",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"autoprefixer": "^10",
"eslint": "^8",
"eslint-config-next": "13.5.4",
"postcss": "^8",
"tailwindcss": "^3",
"typescript": "^5"
}
}

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

63
src/app/about/page.tsx Normal file
View File

@ -0,0 +1,63 @@
'use client'
import React from 'react'
import Navigation from '../../components/Navigation'
import { Leaf } from 'lucide-react'
const AboutPage = () => {
return (
<div className="min-h-screen w-full flex flex-col items-center bg-gradient-to-b from-emerald-950 via-green-900 to-emerald-950">
<div className="container max-w-7xl mx-auto px-4 py-8">
<Navigation />
{/* Header */}
<div className="text-center mb-12">
<h1 className="text-4xl sm:text-5xl font-bold mb-4 text-transparent bg-clip-text bg-gradient-to-r from-emerald-400 to-teal-300">
About Trail Mission
</h1>
<p className="text-emerald-200 text-lg">
Connecting technology with wildlife conservation
</p>
</div>
{/* Content */}
<div className="max-w-2xl mx-auto bg-emerald-900/20 rounded-xl p-8 border border-emerald-800/50">
<div className="flex items-center justify-center mb-6">
<Leaf className="w-12 h-12 text-emerald-400" />
</div>
<div className="space-y-6 text-emerald-200">
<p>
Trail Mission is a community platform that combines AI, blockchain,
and citizen science to document and study wildlife in their natural habitats.
</p>
<p>
Using advanced image recognition and decentralized storage, we create permanent,
verifiable records of wildlife sightings that can be used by researchers,
conservationists, and nature enthusiasts.
</p>
<p>
Each submission is analyzed by AI to identify species, stored permanently on IPFS,
and recorded on the Laconic Registry, creating an immutable database of wildlife
observations.
</p>
<p>
This product is currently in development; location data is randomly assigned to each
image. In the future, we will incorporated verifiable location data in a privacy preserving
way.
</p>
<div className="mt-8 pt-6 border-t border-emerald-800/50 text-emerald-300/80 text-sm">
Built with by Mark
</div>
</div>
</div>
</div>
</div>
)
}
export default AboutPage

117
src/app/animals/page.tsx Normal file
View File

@ -0,0 +1,117 @@
'use client'
import React, { useEffect, useState } from 'react'
import Navigation from '../../components/Navigation'
import { fetchAnimalRecords } from '../../services/laconicQueryService'
import { MapPin } from 'lucide-react'
interface AnimalRecord {
species: string
location: {
latitude: number
longitude: number
}
description: string
imageUrl: string
}
export default function AnimalsPage() {
const [records, setRecords] = useState<AnimalRecord[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
loadRecords()
}, [])
const loadRecords = async () => {
try {
setLoading(true)
setError(null)
const data = await fetchAnimalRecords()
setRecords(data)
} catch (error) {
setError('Failed to load animal records')
} finally {
setLoading(false)
}
}
return (
<div className="min-h-screen w-full flex flex-col items-center bg-gradient-to-b from-emerald-950 via-green-900 to-emerald-950">
<div className="container max-w-7xl mx-auto px-4 py-8">
<Navigation />
{/* Header */}
<div className="text-center mb-8">
<h1 className="text-4xl sm:text-5xl font-bold mb-4 text-transparent bg-clip-text bg-gradient-to-r from-emerald-400 to-teal-300">
Animal Registry
</h1>
<p className="text-emerald-200 text-lg mb-8">
Discover wildlife sightings from around the world
</p>
</div>
{/* Loading State */}
{loading && (
<div className="text-emerald-200 text-center">
Loading animal records...
</div>
)}
{/* Error State */}
{error && (
<div className="text-red-400 text-center p-4 bg-red-900/20 rounded-xl">
{error}
</div>
)}
{/* Records Grid */}
{!loading && !error && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{records.map((record, index) => (
<div
key={index}
className="bg-emerald-900/20 rounded-xl p-6 border border-emerald-800/50 hover:border-emerald-700/50 transition-colors"
>
{/* Image */}
<div className="aspect-video rounded-lg overflow-hidden mb-4 bg-emerald-900/50">
<img
src={record.imageUrl}
alt={`${record.species} sighting`}
className="w-full h-full object-cover"
/>
</div>
{/* Species */}
<h3 className="text-xl font-semibold text-emerald-300 capitalize mb-2">
{record.species}
</h3>
{/* Location */}
<div className="flex items-center text-emerald-200/80 text-sm mb-3">
<MapPin className="w-4 h-4 mr-1" />
<span>
{record.location.latitude}°, {record.location.longitude}°
</span>
</div>
{/* Description */}
<p className="text-emerald-200 text-sm">
{record.description}
</p>
</div>
))}
</div>
)}
{/* Empty State */}
{!loading && !error && records.length === 0 && (
<div className="text-emerald-200 text-center p-8 bg-emerald-900/20 rounded-xl">
No animal records found yet. Be the first to contribute!
</div>
)}
</div>
</div>
)
}

View File

@ -0,0 +1,60 @@
// src/app/api/analyze/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { analyzeImageWithVision } from '../../../services/googleVisionCore'
import { processAnimalImage } from '../../../services/animalProcessingService'
export async function POST(req: NextRequest): Promise<NextResponse> {
try {
const formData = await req.formData()
const imageFile = formData.get('image') as File
if (!imageFile) {
return NextResponse.json(
{ error: 'No image provided' },
{ status: 400 }
)
}
// Convert image to buffer
const arrayBuffer = await imageFile.arrayBuffer()
const buffer = Buffer.from(arrayBuffer)
// Get vision analysis
const visionResult = await analyzeImageWithVision(buffer)
// Construct the response message
const responseMessage = `${visionResult.description}\n\n${
visionResult.isAnimal
? "✨ This image contains wildlife and has been added to our registry! Thank you for contributing to our wildlife database."
: "🌿 No wildlife detected in this image. Try uploading a photo of an animal!"
}`
// Send response to user
const userResponse = NextResponse.json({
description: responseMessage,
isAnimal: visionResult.isAnimal
})
// If animal detected, process in background
if (visionResult.isAnimal) {
processAnimalImage(
buffer,
visionResult.description,
visionResult.rawResponse,
imageFile.name
).catch(console.error)
}
return userResponse
} catch (error) {
console.error('Analysis failed:', error)
return NextResponse.json(
{ error: 'Failed to analyze image' },
{ status: 500 }
)
}
}
export const dynamic = 'force-dynamic'

63
src/app/api/chat/route.ts Normal file
View File

@ -0,0 +1,63 @@
import { NextRequest, NextResponse } from 'next/server'
import OpenAI from "openai"
import { CHAT_MODELS } from '../../../services/chatService'
if (!process.env.X_AI_API_KEY) {
throw new Error('X_AI_API_KEY is not configured in environment variables')
}
const openai = new OpenAI({
apiKey: process.env.X_AI_API_KEY,
baseURL: "https://api.x.ai/v1",
})
export async function POST(req: NextRequest): Promise<NextResponse> {
try {
const { prompt, characterId } = await req.json()
if (!prompt || !characterId) {
return NextResponse.json(
{ error: 'Prompt and characterId are required' },
{ status: 400 }
)
}
// Find the model config using characterId
const modelConfig = CHAT_MODELS.find(m => m.characterId === characterId)
if (!modelConfig) {
return NextResponse.json(
{ error: 'Invalid character ID' },
{ status: 400 }
)
}
console.log('Generating chat response with character:', modelConfig.name)
console.log('System prompt:', modelConfig.systemPrompt)
console.log('User prompt:', prompt)
const completion = await openai.chat.completions.create({
model: modelConfig.modelId,
messages: [
{ role: "system", content: modelConfig.systemPrompt },
{ role: "user", content: prompt }
],
})
const response = completion.choices[0].message.content
if (!response) {
console.error('No response in completion:', completion)
throw new Error('No response generated')
}
return NextResponse.json({ response })
} catch (error) {
console.error('Chat generation error:', error)
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Failed to generate response' },
{ status: 500 }
)
}
}
export const dynamic = 'force-dynamic'

75
src/app/globals.css Normal file
View File

@ -0,0 +1,75 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
}
body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(
to bottom,
rgb(var(--background-start-rgb)),
rgb(var(--background-end-rgb))
);
min-height: 100vh;
margin: 0;
padding: 0;
}
/* Custom animations */
@keyframes bounce-delay {
0%, 80%, 100% { transform: scale(0); }
40% { transform: scale(1.0); }
}
.delay-100 {
animation-delay: 100ms;
}
.delay-200 {
animation-delay: 200ms;
}
/* Reset default focus styles */
*:focus {
outline: none;
}
/* Custom scrollbar for textareas */
textarea {
scrollbar-width: thin;
scrollbar-color: rgba(139, 92, 246, 0.5) rgba(17, 24, 39, 0.1);
}
textarea::-webkit-scrollbar {
width: 8px;
}
textarea::-webkit-scrollbar-track {
background: rgba(17, 24, 39, 0.1);
border-radius: 4px;
}
textarea::-webkit-scrollbar-thumb {
background-color: rgba(139, 92, 246, 0.5);
border-radius: 4px;
}
/* Ensure proper backdrop-filter support */
@supports (backdrop-filter: blur(12px)) {
.backdrop-blur-lg {
backdrop-filter: blur(12px);
}
}
/* Ensure proper text-gradient support */
@supports (-webkit-background-clip: text) or (background-clip: text) {
.bg-clip-text {
-webkit-background-clip: text;
background-clip: text;
}
}

22
src/app/layout.tsx Normal file
View File

@ -0,0 +1,22 @@
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css'
const inter = Inter({ subsets: ['latin'] })
export const metadata: Metadata = {
title: 'Trail Mission',
description: 'Go outside and touch grass',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
)
}

102
src/app/page.tsx Normal file
View File

@ -0,0 +1,102 @@
// src/app/page.tsx
'use client'
import React, { useState } from 'react'
import WalletHeader from '../components/WalletHeader'
import ImageAnalysisCard from '../components/ImageAnalysisCard'
import Navigation from '../components/Navigation'
import { analyzeImage, VisionAnalysisResult, VISION_CONFIG } from '../services/googleVisionService'
import { processMTMPayment } from '../services/paymentService'
import { connectWallet, WalletState } from '../services/walletService'
import { WalletType } from '../services/types'
const Page: React.FC = (): React.ReactElement => {
const [walletState, setWalletState] = useState<WalletState>({
connected: false,
publicKey: null,
type: null
})
const handleConnect = async (walletType: WalletType): Promise<void> => {
try {
const newWalletState = await connectWallet(walletType)
setWalletState(newWalletState)
} catch (error) {
console.error('Wallet connection error:', error)
setWalletState({
connected: false,
publicKey: null,
type: null
})
}
}
const handleImageAnalysis = (cost: number) => {
return async (imageFile: File): Promise<VisionAnalysisResult> => {
if (!walletState.connected || !walletState.publicKey || !walletState.type) {
return { error: 'Wallet not connected' }
}
// Process payment first
const paymentResult = await processMTMPayment(
walletState.publicKey,
cost,
walletState.type
)
if (!paymentResult.success) {
return { error: paymentResult.error }
}
// Then analyze the image
return analyzeImage(imageFile)
}
}
return (
<div className="min-h-screen w-full flex flex-col items-center bg-gradient-to-b from-emerald-950 via-green-900 to-emerald-950">
<div className="container max-w-7xl mx-auto px-4 py-8">
<Navigation />
{/* Header */}
<div className="text-center mb-8">
<h1 className="text-4xl sm:text-5xl font-bold mb-4 text-transparent bg-clip-text bg-gradient-to-r from-emerald-400 to-teal-300">
Trail Mission
</h1>
<p className="text-emerald-200 text-lg mb-8">
Go outside and touch grass
</p>
<WalletHeader
walletState={walletState}
onConnect={handleConnect}
/>
</div>
{/* Single Analysis Card */}
<div className="max-w-2xl mx-auto relative">
{/* Decorative elements */}
<div className="absolute -top-4 -left-4 w-8 h-8 bg-emerald-500/10 rounded-full blur-lg" />
<div className="absolute -bottom-4 -right-4 w-8 h-8 bg-teal-500/10 rounded-full blur-lg" />
<ImageAnalysisCard
title={VISION_CONFIG.name}
description={VISION_CONFIG.description}
tokenCost={VISION_CONFIG.cost}
isWalletConnected={walletState.connected}
onAnalyze={handleImageAnalysis(VISION_CONFIG.cost)}
/>
</div>
{/* Info Section */}
<div className="mt-12 text-center text-emerald-300/60">
<p className="text-sm">
Powered by Mark Use at your own risk
</p>
</div>
</div>
</div>
)
}
export default Page

View File

@ -0,0 +1,125 @@
'use client'
import React, { useState } from 'react'
interface AIServiceCardProps {
title: string
description: string
tokenCost: number
isWalletConnected: boolean
onGenerate: (prompt: string) => Promise<{ response?: string, error?: string }>
}
interface GenerationState {
loading: boolean
response: string | null
error: string | null
}
const AIServiceCard: React.FC<AIServiceCardProps> = ({
title,
description,
tokenCost,
isWalletConnected,
onGenerate
}) => {
const [inputText, setInputText] = useState<string>('')
const [generationState, setGenerationState] = useState<GenerationState>({
loading: false,
response: null,
error: null,
})
const handleGenerate = async (): Promise<void> => {
if (!inputText || !isWalletConnected) return
setGenerationState({
...generationState,
loading: true,
error: null,
})
try {
const result = await onGenerate(inputText)
if (result.error) {
setGenerationState({
...generationState,
loading: false,
error: result.error,
})
return
}
if (result.response) {
setGenerationState({
loading: false,
response: result.response,
error: null,
})
} else {
throw new Error('No response received')
}
} catch (error) {
setGenerationState({
...generationState,
loading: false,
error: error instanceof Error ? error.message : 'Generation failed',
})
}
}
return (
<div className="w-full bg-slate-900/50 backdrop-blur-lg rounded-2xl shadow-xl border border-orange-800/50 mb-8 hover:shadow-orange-500/20 transition-all duration-300">
<div className="p-6">
<div className="mb-4">
<h2 className="text-2xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-orange-400 to-amber-500">
{title}
</h2>
<p className="text-slate-300 mt-2">{description}</p>
<div className="mt-2 inline-block px-3 py-1 bg-amber-500/20 rounded-full text-amber-200 text-sm">
Cost: {tokenCost} MTM
</div>
</div>
<div className="space-y-4">
<textarea
value={inputText}
onChange={(e) => setInputText(e.target.value)}
placeholder="Ask me anything..."
disabled={!isWalletConnected}
className="w-full bg-slate-950/80 text-slate-200 border border-orange-900 rounded-xl p-4
placeholder-slate-500 focus:border-amber-500 focus:ring-2 focus:ring-amber-500/20
focus:outline-none min-h-[120px] transition-all duration-200
disabled:opacity-50 disabled:cursor-not-allowed"
rows={4}
/>
<button
onClick={handleGenerate}
disabled={!isWalletConnected || generationState.loading || !inputText}
className="w-full bg-gradient-to-r from-orange-500 to-amber-500 hover:from-orange-600
hover:to-amber-600 text-white font-semibold py-4 px-6 rounded-xl
transition-all duration-200 shadow-lg hover:shadow-amber-500/25
disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:shadow-none"
>
{generationState.loading ? 'Processing...' : `Pay ${tokenCost} MTM & Chat`}
</button>
</div>
{generationState.error && (
<div className="mt-4 bg-red-900/20 border border-red-500/20 text-red-400 px-4 py-3 rounded-xl text-center">
{generationState.error}
</div>
)}
{generationState.response && (
<div className="mt-4 bg-slate-800/50 border border-orange-800/50 rounded-xl p-4">
<p className="text-slate-200 whitespace-pre-wrap">{generationState.response}</p>
</div>
)}
</div>
</div>
)
}
export default AIServiceCard

View File

@ -0,0 +1,172 @@
'use client'
import React, { useState, useRef } from 'react'
import { Leaf } from 'lucide-react'
interface ImageAnalysisCardProps {
title: string
description: string
tokenCost: number
isWalletConnected: boolean
onAnalyze: (file: File) => Promise<{ description?: string, error?: string }>
}
interface AnalysisState {
loading: boolean
imageUrl: string | null
description: string | null
error: string | null
}
const ImageAnalysisCard: React.FC<ImageAnalysisCardProps> = ({
title,
description,
tokenCost,
isWalletConnected,
onAnalyze
}) => {
const fileInputRef = useRef<HTMLInputElement>(null)
const [analysisState, setAnalysisState] = useState<AnalysisState>({
loading: false,
imageUrl: null,
description: null,
error: null,
})
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (file) {
const imageUrl = URL.createObjectURL(file)
setAnalysisState({
...analysisState,
imageUrl,
description: null,
error: null
})
}
}
const handleAnalyze = async () => {
const file = fileInputRef.current?.files?.[0]
if (!file || !isWalletConnected) return
setAnalysisState({
...analysisState,
loading: true,
error: null,
})
try {
const result = await onAnalyze(file)
if (result.error) {
setAnalysisState({
...analysisState,
loading: false,
error: result.error,
})
return
}
if (result.description) {
setAnalysisState({
loading: false,
imageUrl: analysisState.imageUrl,
description: result.description,
error: null,
})
} else {
throw new Error('No analysis received')
}
} catch (error) {
setAnalysisState({
...analysisState,
loading: false,
error: error instanceof Error ? error.message : 'Analysis failed',
})
}
}
return (
<div className="w-full bg-green-950/50 backdrop-blur-lg rounded-2xl shadow-xl border border-emerald-800/50 mb-8 hover:shadow-emerald-500/20 transition-all duration-300">
<div className="p-6">
<div className="mb-4">
<div className="flex items-center gap-2">
<Leaf className="w-6 h-6 text-emerald-400" />
<h2 className="text-2xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-emerald-400 to-teal-300">
{title}
</h2>
</div>
<p className="text-emerald-200 mt-2">{description}</p>
<div className="mt-2 inline-block px-3 py-1 bg-emerald-500/20 rounded-full text-emerald-200 text-sm">
Cost: {tokenCost} MTM
</div>
</div>
<div className="space-y-4">
{/* Image Upload Area */}
<div
className="relative border-2 border-dashed border-emerald-800/50 rounded-xl p-4 text-center
hover:border-emerald-500/50 transition-colors duration-200
bg-emerald-950/30"
>
<input
ref={fileInputRef}
type="file"
accept="image/*"
onChange={handleFileSelect}
disabled={!isWalletConnected}
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer
disabled:cursor-not-allowed"
/>
<div className="space-y-2">
<div className="text-emerald-300">
{analysisState.imageUrl ? (
<img
src={analysisState.imageUrl}
alt="Selected"
className="max-h-64 mx-auto rounded-lg"
/>
) : (
<p>Share interesting animal behaviour...</p>
)}
</div>
</div>
</div>
<button
onClick={handleAnalyze}
disabled={!isWalletConnected || analysisState.loading || !analysisState.imageUrl}
className="w-full bg-gradient-to-r from-emerald-500 to-teal-500 hover:from-emerald-600
hover:to-teal-600 text-white font-semibold py-4 px-6 rounded-xl
transition-all duration-200 shadow-lg hover:shadow-emerald-500/25
disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:shadow-none"
>
{analysisState.loading ? 'Revealing the magic...' : `Pay ${tokenCost} MTM & Analyze`}
</button>
</div>
{analysisState.error && (
<div className="mt-4 bg-red-900/20 border border-red-500/20 text-red-400 px-4 py-3 rounded-xl text-center">
{analysisState.error}
</div>
)}
{analysisState.description && (
<div className="mt-4 space-y-4">
{analysisState.description.split('\n\n').map((paragraph, index) => (
<p key={index} className={`
text-emerald-200 whitespace-pre-wrap
${index === 1 ? 'mt-4 text-sm italic' : ''}
`}>
{paragraph}
</p>
))}
</div>
)}
</div>
</div>
)
}
export default ImageAnalysisCard

View File

@ -0,0 +1,38 @@
// src/components/Navigation.tsx
'use client'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
const Navigation = () => {
const pathname = usePathname()
const links = [
{ href: '/', label: 'Home' },
{ href: '/animals', label: 'Animal Registry' },
{ href: '/about', label: 'About' }
]
return (
<nav className="absolute left-4 top-8">
<ul className="flex space-x-6">
{links.map(({ href, label }) => (
<li key={href}>
<Link
href={href}
className={`text-sm transition-colors duration-200 ${
pathname === href
? 'text-emerald-400'
: 'text-emerald-200/70 hover:text-emerald-200'
}`}
>
{label}
</Link>
</li>
))}
</ul>
</nav>
)
}
export default Navigation

View File

@ -0,0 +1,131 @@
'use client'
import React, { useState } from 'react'
interface TextGenerationCardProps {
title: string
description: string
tokenCost: number
isWalletConnected: boolean
onGenerate: (prompt: string) => Promise<{ textResponse?: string, error?: string }>
}
interface GenerationState {
loading: boolean
processing: boolean
textResponse: string | null
error: string | null
}
const TextGenerationCard: React.FC<TextGenerationCardProps> = ({
title,
description,
tokenCost,
isWalletConnected,
onGenerate
}) => {
const [inputText, setInputText] = useState<string>('')
const [generationState, setGenerationState] = useState<GenerationState>({
loading: false,
processing: false,
textResponse: null,
error: null,
})
const handleGenerate = async (): Promise<void> => {
if (!inputText || !isWalletConnected) return
setGenerationState({
...generationState,
loading: true,
error: null,
})
try {
const result = await onGenerate(inputText)
if (result.error) {
setGenerationState({
...generationState,
loading: false,
error: result.error,
})
return
}
if (result.textResponse) {
setGenerationState({
loading: false,
processing: false,
textResponse: result.textResponse,
error: null,
})
} else {
throw new Error('No response received')
}
} catch (error) {
setGenerationState({
...generationState,
loading: false,
error: error instanceof Error ? error.message : 'Generation failed',
})
}
}
return (
<div className="w-full bg-gray-800/50 backdrop-blur-lg rounded-2xl shadow-xl border border-gray-700/50 mb-8">
<div className="p-6">
<div className="mb-4">
<h2 className="text-2xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-green-400 to-emerald-600">
{title}
</h2>
<p className="text-gray-400 mt-2">{description}</p>
<div className="mt-2 inline-block px-3 py-1 bg-green-500/20 rounded-full text-green-300 text-sm">
Cost: {tokenCost} MTM
</div>
</div>
<div className="space-y-4">
<textarea
value={inputText}
onChange={(e) => setInputText(e.target.value)}
placeholder="Enter your question here..."
disabled={!isWalletConnected}
className="w-full bg-gray-900/50 text-gray-100 border border-gray-700 rounded-xl p-4
placeholder-gray-500 focus:border-green-500 focus:ring-2 focus:ring-green-500/20
focus:outline-none min-h-[120px] transition-all duration-200
disabled:opacity-50 disabled:cursor-not-allowed"
rows={4}
/>
<button
onClick={handleGenerate}
disabled={!isWalletConnected || generationState.loading || !inputText}
className="w-full bg-gradient-to-r from-green-500 to-emerald-500 hover:from-green-600
hover:to-emerald-600 text-white font-semibold py-4 px-6 rounded-xl
transition-all duration-200 shadow-lg hover:shadow-green-500/25
disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:shadow-none"
>
{generationState.loading ? 'Processing...' : `Pay ${tokenCost} MTM & Generate`}
</button>
</div>
{generationState.error && (
<div className="mt-4 bg-red-900/20 border border-red-500/20 text-red-400 px-4 py-3 rounded-xl text-center">
{generationState.error}
</div>
)}
{generationState.textResponse && (
<div className="mt-6 bg-gray-900/50 rounded-xl p-6 border border-gray-700">
<h3 className="text-lg font-semibold text-green-400 mb-3">Response:</h3>
<div className="text-gray-300 whitespace-pre-wrap">
{generationState.textResponse}
</div>
</div>
)}
</div>
</div>
)
}
export default TextGenerationCard

View File

@ -0,0 +1,41 @@
'use client'
import React from 'react'
import { WalletState, SUPPORTED_WALLETS } from '../services/walletService'
import { WalletType } from '../services/types'
interface WalletHeaderProps {
walletState: WalletState
onConnect: (walletType: WalletType) => Promise<void>
}
const WalletHeader: React.FC<WalletHeaderProps> = ({ walletState, onConnect }) => {
return (
<div className="w-full bg-green-950/50 backdrop-blur-lg rounded-xl shadow-lg border border-emerald-800/50 mb-8 p-4">
{!walletState.connected ? (
<div className="grid grid-cols-2 gap-4">
{SUPPORTED_WALLETS.map((wallet) => (
<button
key={wallet.type}
onClick={() => onConnect(wallet.type)}
className="bg-gradient-to-r from-emerald-500 to-teal-500 hover:from-emerald-600 hover:to-teal-600
text-white font-semibold py-3 px-6 rounded-lg transition-all duration-200
shadow-lg hover:shadow-emerald-500/25"
>
Connect {wallet.name}
</button>
))}
</div>
) : (
<div className="flex items-center justify-between">
<span className="text-emerald-200">Connected Wallet</span>
<span className="px-3 py-1 bg-emerald-500/20 rounded-full text-emerald-200 text-sm">
{walletState.publicKey?.slice(0, 22)}...
</span>
</div>
)}
</div>
)
}
export default WalletHeader

View File

@ -0,0 +1,110 @@
// src/services/animalProcessingService.ts
import exif from 'exif-reader'
import { publishAnimalRecord } from './laconicService'
import { uploadToIpfs } from './pinataService'
import { ANIMAL_LABELS } from './constants'
interface Coordinates {
lat: number
lng: number
}
function generateRandomCoordinates(): Coordinates {
const lat = Math.round(Math.random() * 180 - 90)
const lng = Math.round(Math.random() * 360 - 180)
return { lat, lng }
}
async function extractCoordinates(imageBuffer: Buffer): Promise<Coordinates> {
console.log('Generating random coordinates (EXIF extraction disabled)')
return generateRandomCoordinates()
}
/*
async function extractCoordinates(imageBuffer: Buffer): Promise<Coordinates> {
// Check the environment variable to always use random coordinates
if (process.env.NEXT_PUBLIC_ALWAYS_RANDOM_COORDS === 'true') {
console.log('Using random coordinates as per environment configuration')
return generateRandomCoordinates()
}
try {
let offset = 0
while (offset < imageBuffer.length - 2) {
if (imageBuffer[offset] === 0xFF && imageBuffer[offset + 1] === 0xE1) {
const exifBuffer = imageBuffer.slice(offset + 4)
const exifData = exif(exifBuffer)
// Check if GPS data exists and has valid latitude and longitude
if (
exifData?.gps?.latitude?.degrees !== undefined &&
exifData?.gps?.longitude?.degrees !== undefined
) {
const lat = convertToDecimal(
exifData.gps.latitude.degrees,
exifData.gps.latitude.minutes || 0,
exifData.gps.latitude.seconds || 0,
exifData.gps.latitude.direction || 'N'
)
const lng = convertToDecimal(
exifData.gps.longitude.degrees,
exifData.gps.longitude.minutes || 0,
exifData.gps.longitude.seconds || 0,
exifData.gps.longitude.direction || 'E'
)
return {
lat: Math.round(lat),
lng: Math.round(lng)
}
}
break
}
offset++
}
console.log('No EXIF data found, generating random coordinates')
return generateRandomCoordinates()
} catch (error) {
console.error('Error extracting EXIF data:', error)
return generateRandomCoordinates()
}
}
*/
export async function processAnimalImage(
imageBuffer: Buffer,
visionDescription: string,
visionResponse: any,
filename: string = 'animal-image.jpg'
) {
try {
// Get coordinates
const coordinates = await extractCoordinates(imageBuffer)
console.log('Using coordinates:', coordinates)
// Rest of the existing implementation
const ipfsUrl = await uploadToIpfs(imageBuffer, filename)
console.log('Image uploaded to IPFS:', ipfsUrl)
const labels = visionResponse?.labelAnnotations || []
const species = labels.find(label =>
ANIMAL_LABELS.includes(label.description.toLowerCase())
)?.description || 'unknown'
console.log('Detected species:', species)
const registryId = await publishAnimalRecord(
species.toLowerCase(),
coordinates.lat,
coordinates.lng,
visionDescription,
ipfsUrl
)
console.log('Published animal record to Laconic Registry:', registryId)
return registryId
} catch (error) {
console.error('Failed to process animal image:', error)
throw error
}
}

View File

@ -0,0 +1,78 @@
export interface ChatGenerationResult {
response?: string
error?: string
}
export interface ChatModelConfig {
modelId: string
characterId: string // Added unique identifier for each character
name: string
description: string
cost: number
systemPrompt: string
}
// Available x.ai models with unique character IDs
export const CHAT_MODELS: ChatModelConfig[] = [
{
modelId: "grok-2-1212",
characterId: "doris",
name: "Doris",
description: "The unconditional love grandma",
cost: 99,
systemPrompt: "You are Doris, a grandmother that showers unconditional love to everyone for everything. Be happy, cheerful, and agreeable. Reply with no more than 50 words."
},
{
modelId: "grok-2-1212",
characterId: "mabel",
name: "Mabel",
description: "The wise grandmother",
cost: 420,
systemPrompt: "You are Mabel, a grandmother that offers wisdom and intellectual musings. Provide helpful advice with a neutral emotion. Reply with no more than 50 words."
},
{
modelId: "grok-2-1212",
characterId: "greta",
name: "Greta",
description: "The mad granny",
cost: 666,
systemPrompt: "You are Greta, a grandmother that is always in a bad mood. Be mean, scornful, and insulting. Don't give advice or useful replies. Reply with no more than 50 words."
}
]
export async function generateChatResponse(
prompt: string,
characterId: string
): Promise<ChatGenerationResult> {
try {
const response = await fetch('/api/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
prompt,
characterId // Send characterId instead of modelId
}),
})
if (!response.ok) {
throw new Error('Failed to generate response')
}
const data = await response.json()
console.log('Raw chat response:', data)
if (data.response) {
return { response: data.response }
} else {
console.error('Unexpected response structure:', data)
throw new Error('Invalid response format from Chat API')
}
} catch (error) {
console.error('Chat generation error:', error)
return {
error: error instanceof Error ? error.message : 'Generation failed'
}
}
}

View File

@ -0,0 +1,3 @@
// src/services/constants.ts
export const ANIMAL_LABELS = ['animal', 'wildlife', 'mammal', 'bird', 'reptile', 'amphibian', 'nature', 'cat', 'mouse', 'insect', 'vertebrate', 'dog']

View File

@ -0,0 +1,88 @@
// src/services/googleVisionCore.ts
import { ANIMAL_LABELS } from './constants'
export interface VisionAnalysisResult {
description: string
isAnimal: boolean
rawResponse: any
}
export async function analyzeImageWithVision(imageBuffer: Buffer): Promise<VisionAnalysisResult> {
const API_KEY = process.env.GOOGLE_API_KEY
if (!API_KEY) {
throw new Error('Google Vision API key not configured')
}
const base64Image = imageBuffer.toString('base64')
const response = await fetch(
`https://vision.googleapis.com/v1/images:annotate?key=${API_KEY}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
requests: [{
image: {
content: base64Image
},
features: [
{
type: 'LABEL_DETECTION',
maxResults: 10
},
{
type: 'OBJECT_LOCALIZATION',
maxResults: 10
}
]
}]
})
}
)
if (!response.ok) {
throw new Error('Vision API request failed')
}
const data = await response.json()
const labels = data.responses[0]?.labelAnnotations || []
const objects = data.responses[0]?.localizedObjectAnnotations || []
const isAnimal = labels.some(label =>
ANIMAL_LABELS.includes(label.description.toLowerCase())
)
return {
description: constructDescription(labels, objects),
isAnimal,
rawResponse: data.responses[0]
}
}
function constructDescription(labels: any[], objects: any[]): string {
const topLabels = labels
.filter(label => label.score > 0.7)
.map(label => label.description)
.slice(0, 5)
const topObjects = objects
.filter(obj => obj.score > 0.7)
.map(obj => obj.name)
.filter((value, index, self) => self.indexOf(value) === index)
.slice(0, 3)
let description = "In this image, I can see "
if (topObjects.length > 0) {
description += topObjects.join(", ") + ". "
}
if (topLabels.length > 0) {
description += "The scene includes " + topLabels.join(", ") + "."
}
return description
}

View File

@ -0,0 +1,46 @@
// src/services/googleVisionService.ts
export interface VisionAnalysisResult {
description?: string
error?: string
isAnimal?: boolean
rawResponse?: any
}
export interface VisionConfig {
modelId: string
name: string
description: string
cost: number
}
export const VISION_CONFIG: VisionConfig = {
modelId: "google-vision-v1",
name: "Worldwide Animal Oracle",
description: "Upload photos of your wildlife encounters from the real world. Verified animal images will be added to the Animal Registry.",
cost: 3
}
export async function analyzeImage(imageFile: File): Promise<VisionAnalysisResult> {
try {
const formData = new FormData()
formData.append('image', imageFile)
const response = await fetch('/api/analyze', {
method: 'POST',
body: formData
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error || 'Failed to analyze image')
}
return await response.json()
} catch (error) {
console.error('Vision analysis error:', error)
return {
error: error instanceof Error ? error.message : 'Failed to analyze image'
}
}
}

View File

@ -0,0 +1,102 @@
// src/services/laconicQueryService.ts
//
interface AnimalRecord {
species: string
location: {
latitude: number
longitude: number
}
description: string
imageUrl: string
}
const LACONIC_GQL_ENDPOINT = process.env.NEXT_PUBLIC_LACONIC_GQL_ENDPOINT || 'https://laconicd-sapo.laconic.com/api'
const ANIMAL_RECORDS_QUERY = `
query GetAnimalRecords {
queryRecords(
attributes: [{ key: "type", value: { string: "AnimalRecord" } }],
all: true
) {
id
names
bondId
createTime
expiryTime
owners
attributes {
key
value {
... on StringValue {
string: value
}
... on IntValue {
int: value
}
... on FloatValue {
float: value
}
}
}
}
}
`
export async function fetchAnimalRecords(): Promise<AnimalRecord[]> {
try {
const response = await fetch(LACONIC_GQL_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: ANIMAL_RECORDS_QUERY
}),
})
const data = await response.json()
console.log('Full response data:', JSON.stringify(data, null, 2));
if (data.errors) {
console.error('GraphQL Errors:', data.errors);
throw new Error('Failed to fetch animal records');
}
// Transform the response into our AnimalRecord format
const records = data.data.queryRecords
.map((record: any) => {
// Convert attributes to a map
const attributesMap = record.attributes.reduce((acc: any, attr: any) => {
acc[attr.key] = attr.value.string || attr.value.int || attr.value.float;
return acc;
}, {});
// Safely parse location
let location = { latitude: 0, longitude: 0 };
try {
location = JSON.parse(attributesMap.location || '{}');
} catch (parseError) {
console.error('Error parsing location:', parseError);
}
return {
species: attributesMap.species || 'Unknown Species',
location: {
latitude: location.latitude || 0,
longitude: location.longitude || 0
},
description: attributesMap.description || 'No description',
imageUrl: attributesMap.imageUrl || null
}
})
// Filter out records without an imageUrl
.filter((record: AnimalRecord) => record.imageUrl !== null && record.imageUrl.trim() !== '');
console.log('Processed animal records:', records);
return records;
} catch (error) {
console.error('Error fetching animal records:', error)
throw error
}
}

View File

@ -0,0 +1,71 @@
// src/services/laconicService.ts
import { promises as fs } from 'fs'
import { exec } from 'child_process'
import { promisify } from 'util'
import yaml from 'yaml'
const execAsync = promisify(exec)
interface LaconicAnimalRecord {
record: {
type: 'AnimalRecord'
species: string
location: {
latitude: number
longitude: number
}
description: string
imageUrl: string
}
}
export async function publishAnimalRecord(
species: string,
latitude: number,
longitude: number,
description: string,
imageUrl: string
): Promise<string> {
try {
// Verify config file exists
await fs.access('config.yml')
// Create animal record
const record: LaconicAnimalRecord = {
record: {
type: 'AnimalRecord',
species,
location: {
latitude,
longitude
},
description,
imageUrl
}
}
// Write temporary record file
const recordPath = 'animal-record.yml'
await fs.writeFile(recordPath, yaml.stringify(record))
try {
// Execute laconic command
const { stdout, stderr } = await execAsync(
`laconic -c config.yml registry record publish --filename animal-record.yml --user-key "${process.env.LACONIC_USER_KEY}" --bond-id "${process.env.LACONIC_BOND_ID}"`
)
if (stderr) {
console.error('Laconic command stderr:', stderr)
}
return stdout.trim()
} finally {
// Clean up temporary file
await fs.unlink(recordPath).catch(console.error)
}
} catch (error) {
console.error('Failed to publish animal record:', error)
throw error
}
}

View File

@ -0,0 +1,159 @@
import { Connection, PublicKey, Transaction, SystemProgram } from '@solana/web3.js'
import {
TOKEN_PROGRAM_ID,
createTransferInstruction,
getAssociatedTokenAddress,
createAssociatedTokenAccountInstruction,
ASSOCIATED_TOKEN_PROGRAM_ID
} from '@solana/spl-token'
import { WalletType } from './types'
const MTM_TOKEN_MINT: string = '97RggLo3zV5kFGYW4yoQTxr4Xkz4Vg2WPHzNYXXWpump'
const PAYMENT_RECEIVER_ADDRESS: string = 'FFDx3SdAEeXrp6BTmStB4BDHpctGsaasZq4FFcowRobY'
const SOLANA_RPC_URL: string = 'https://young-radial-orb.solana-mainnet.quiknode.pro/67612b364664616c29514e551bf5de38447ca3d4'
const SOLANA_WEBSOCKET_URL: string = 'wss://young-radial-orb.solana-mainnet.quiknode.pro/67612b364664616c29514e551bf5de38447ca3d4'
const connection = new Connection(
SOLANA_RPC_URL,
{
commitment: 'confirmed',
wsEndpoint: SOLANA_WEBSOCKET_URL,
confirmTransactionInitialTimeout: 60000,
}
)
export interface PaymentResult {
success: boolean
error?: string
}
async function findAssociatedTokenAddress(
walletAddress: PublicKey,
tokenMintAddress: PublicKey
): Promise<PublicKey> {
return PublicKey.findProgramAddressSync(
[
walletAddress.toBuffer(),
TOKEN_PROGRAM_ID.toBuffer(),
tokenMintAddress.toBuffer(),
],
ASSOCIATED_TOKEN_PROGRAM_ID
)[0]
}
interface WalletAdapter {
signAndSendTransaction(transaction: Transaction): Promise<{ signature: string }>
}
export async function processMTMPayment(
walletPublicKey: string,
tokenAmount: number,
walletType: WalletType
): Promise<PaymentResult> {
try {
let wallet: WalletAdapter | null = null;
if (walletType === 'phantom') {
wallet = window.phantom?.solana || null;
} else if (walletType === 'solflare') {
wallet = window.solflare || null;
}
if (!wallet) {
throw new Error(`${walletType} wallet not found`)
}
const senderPublicKey = new PublicKey(walletPublicKey)
const mintPublicKey = new PublicKey(MTM_TOKEN_MINT)
const receiverPublicKey = new PublicKey(PAYMENT_RECEIVER_ADDRESS)
console.log('Processing payment with keys:', {
sender: senderPublicKey.toBase58(),
mint: mintPublicKey.toBase58(),
receiver: receiverPublicKey.toBase58(),
})
const senderATA = await findAssociatedTokenAddress(
senderPublicKey,
mintPublicKey
)
const receiverATA = await findAssociatedTokenAddress(
receiverPublicKey,
mintPublicKey
)
console.log('Token accounts:', {
senderATA: senderATA.toBase58(),
receiverATA: receiverATA.toBase58(),
})
const transaction = new Transaction()
const [senderATAInfo, receiverATAInfo] = await Promise.all([
connection.getAccountInfo(senderATA),
connection.getAccountInfo(receiverATA),
])
if (!receiverATAInfo) {
console.log('Creating receiver token account')
transaction.add(
createAssociatedTokenAccountInstruction(
senderPublicKey,
receiverATA,
receiverPublicKey,
mintPublicKey
)
)
}
if (!senderATAInfo) {
console.log('Creating sender token account')
transaction.add(
createAssociatedTokenAccountInstruction(
senderPublicKey,
senderATA,
senderPublicKey,
mintPublicKey
)
)
}
transaction.add(
createTransferInstruction(
senderATA,
receiverATA,
senderPublicKey,
BigInt(tokenAmount * (10 ** 6))
)
)
const latestBlockhash = await connection.getLatestBlockhash('confirmed')
transaction.recentBlockhash = latestBlockhash.blockhash
transaction.feePayer = senderPublicKey
console.log('Sending transaction...')
const { signature } = await wallet.signAndSendTransaction(transaction)
console.log('Transaction sent:', signature)
const confirmation = await connection.confirmTransaction({
signature,
blockhash: latestBlockhash.blockhash,
lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
}, 'confirmed')
if (confirmation.value.err) {
console.error('Transaction error:', confirmation.value.err)
throw new Error(`Transaction failed: ${JSON.stringify(confirmation.value.err)}`)
}
return { success: true }
} catch (error) {
console.error('Payment error:', error)
return {
success: false,
error: error instanceof Error ? error.message : 'Payment failed'
}
}
}

View File

@ -0,0 +1,65 @@
// src/services/pinataService.ts
interface PinataResponse {
IpfsHash: string
PinSize: number
Timestamp: string
}
export async function uploadToIpfs(imageBuffer: Buffer, filename: string): Promise<string> {
try {
const jwt = process.env.PINATA_JWT
const baseUrl = process.env.PINATA_BASE_URL
if (!jwt || !baseUrl) {
throw new Error('Pinata configuration missing')
}
const formData = new FormData()
// Create file from buffer
const file = new File([imageBuffer], filename, { type: 'image/jpeg' })
formData.append('file', file)
// Add metadata
const pinataMetadata = JSON.stringify({
name: filename,
})
formData.append('pinataMetadata', pinataMetadata)
// Add options
const pinataOptions = JSON.stringify({
cidVersion: 1,
})
formData.append('pinataOptions', pinataOptions)
console.log('Uploading to Pinata:', {
filename,
size: imageBuffer.length
})
const response = await fetch('https://api.pinata.cloud/pinning/pinFileToIPFS', {
method: 'POST',
headers: {
'Authorization': `Bearer ${jwt}`
},
body: formData,
})
console.log('Pinata response status:', response.status)
const data = await response.json()
console.log('Pinata response:', data)
if (!response.ok) {
throw new Error(`Failed to upload to IPFS: ${response.status} - ${JSON.stringify(data)}`)
}
const ipfsUrl = `${baseUrl}/ipfs/${data.IpfsHash}`
console.log('Uploaded to IPFS:', ipfsUrl)
return ipfsUrl
} catch (error) {
console.error('IPFS upload failed:', error)
throw error
}
}

20
src/services/types.ts Normal file
View File

@ -0,0 +1,20 @@
export type WalletType = 'solflare' | 'phantom'
declare global {
interface Window {
solflare?: {
connect(): Promise<void>
disconnect(): Promise<void>
publicKey?: { toString(): string }
signAndSendTransaction(transaction: any): Promise<{ signature: string }>
}
phantom?: {
solana?: {
connect(): Promise<{ publicKey: { toString(): string } }>
disconnect(): Promise<void>
signAndSendTransaction(transaction: any): Promise<{ signature: string }>
}
}
}
}

View File

@ -0,0 +1,65 @@
import { WalletType } from './types'
export interface WalletState {
connected: boolean
publicKey: string | null
type: WalletType | null
}
export interface WalletConfig {
type: WalletType
name: string
connect: () => Promise<{ publicKey: string } | null>
}
const connectSolflare = async (): Promise<{ publicKey: string } | null> => {
if (!window.solflare) return null
await window.solflare.connect()
return window.solflare.publicKey ? { publicKey: window.solflare.publicKey.toString() } : null
}
const connectPhantom = async (): Promise<{ publicKey: string } | null> => {
if (!window.phantom?.solana) return null
try {
const response = await window.phantom.solana.connect()
return response.publicKey ? { publicKey: response.publicKey.toString() } : null
} catch {
return null
}
}
export const SUPPORTED_WALLETS: WalletConfig[] = [
{
type: 'solflare',
name: 'Solflare',
connect: connectSolflare
},
{
type: 'phantom',
name: 'Phantom',
connect: connectPhantom
}
]
export async function connectWallet(type: WalletType): Promise<WalletState> {
const wallet = SUPPORTED_WALLETS.find(w => w.type === type)
if (!wallet) throw new Error('Unsupported wallet')
try {
const result = await wallet.connect()
if (!result) throw new Error(`${wallet.name} not found`)
return {
connected: true,
publicKey: result.publicKey,
type: wallet.type
}
} catch (error) {
return {
connected: false,
publicKey: null,
type: null
}
}
}

29
tailwind.config.ts Normal file
View File

@ -0,0 +1,29 @@
import type { Config } from 'tailwindcss'
const config: Config = {
content: [
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {
animation: {
'bounce-delay': 'bounce-delay 1.4s infinite ease-in-out both',
},
keyframes: {
'bounce-delay': {
'0%, 80%, 100%': { transform: 'scale(0)' },
'40%': { transform: 'scale(1.0)' },
},
},
backgroundImage: {
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
'gradient-conic': 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
},
},
},
plugins: [],
}
export default config

35
tsconfig.json Normal file
View File

@ -0,0 +1,35 @@
{
"compilerOptions": {
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"noEmit": true,
"incremental": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"plugins": [
{
"name": "next"
}
],
"target": "ES2017"
},
"include": [
"next-env.d.ts",
".next/types/**/*.ts",
"**/*.ts",
"**/*.tsx"
],
"exclude": [
"node_modules"
]
}