chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:30:30 +08:00
commit 914fea506e
2793 changed files with 802106 additions and 0 deletions
@@ -0,0 +1,12 @@
node_modules
npm-debug.log
tests
coverage
Dockerfile
.dockerignore
.env
*.md
.git
.vscode
.gitignore
Makefile
@@ -0,0 +1,287 @@
# Quickbot - Document Search using Agent Builder
Quickbot Document Search Template is a powerful document search application built with Google Cloud's Agent Builder. It features a user-friendly frontend interface and a robust backend API to deliver efficient search capabilities.
## Overview
This project allows users to search through documents indexed by Google Cloud's Vertex AI Search (formerly Generative AI App Builder). It's designed with a decoupled frontend and backend architecture, suitable for scalable deployments.
## Demo
Here's how the Document Search Template provides answers from your documents:
![Document Search Demo](./assets/quickbot-document-search.png)
## Prerequisites
Before you begin, ensure you have the following installed:
* **Docker and Docker Compose v2:** Essential for the containerized deployment.
* Verify your Docker Compose version with `docker compose version`. If you have an older `docker-compose` (with a hyphen), you might need to upgrade to use `docker compose` in the commands.
* **Google Cloud SDK (`gcloud` CLI):** Required for manual backend setup, authentication, and interaction with Google Cloud services.
* **Python 3.x:** For backend development (if not using Docker).
* **Node.js and npm (or yarn):** For frontend development (if not using Docker).
## Getting Started
You have two main options to get the application running:
### Option 1: Using Docker Compose (Recommended for Quick Start)
This is the **simplest way to get the entire application (frontend and backend) up and running!** You just need to run `docker compose up` and you will be all set! But in order to do that, you may need to authenticate with gcloud. See the next steps:
1. **Ensure Docker and Docker Compose v2 are installed and running.**
2. **Authenticate with Google Cloud for Datastore/Engine Creation:**
If you intend for the application (specifically the backend running in Docker) to create or interact with Google Cloud Datastores and Search Engines, you need to provide Google Cloud credentials to the Docker container. The recommended way for local development is using Application Default Credentials (ADC).
Run the following commands in your local terminal:
```bash
gcloud auth application-default login
gcloud config set project <your-project-id>
gcloud auth application-default set-quota-project <your-project-id>
# Verify your configuration
gcloud auth list
gcloud config list project
```
This will create or update ADC on your local machine. The `docker-compose.yml` file is typically configured to mount these local credentials into the backend container, allowing it to authenticate.
> **Windows Users:** The path to ADC might differ on Windows. You may need to adjust the commented-out volume mount paths for `gcloud` credentials in the `docker-compose.yml` file to ensure the backend container can access them.
3. **Build Docker Images (with Datastore Setup Choice):**
The Docker setup includes a build-time argument `IS_FIRST_DEPLOYMENT` to facilitate initial Google Cloud Vertex AI Search datastore and engine creation.
* **Automatic Default Setup (with Public Data):**
If you're running this for the first time and want a quick start with sample data (and have authenticated as per step 2), build the Docker images with the `IS_FIRST_DEPLOYMENT` argument set to `"True"`:
```bash
docker compose build --build-arg IS_FIRST_DEPLOYMENT="True"
```
This configuration will automatically create a default datastore and search engine in your Google Cloud project, populated with public documents from Alphabet. These documents are sourced from [Google's official guide](https://cloud.google.com/generative-ai-app-builder/docs/try-enterprise-search#unstructured-data) for trying Enterprise Search with unstructured data.
* **Custom Setup or Existing Datastore:**
If you do not set `IS_FIRST_DEPLOYMENT="True"` during the build (or set it to any other value, or omit it), the application will not attempt to create the default datastore. Instead, you'll need to:
* Configure the backend with environment variables to connect to an existing datastore/engine.
* Provide specific environment variables that instruct the backend on how to create a custom datastore/engine if desired.
Refer to the "Environment Variables" section for backend configuration details. If you are using this path, you can build without the extra argument:
```bash
docker compose build
```
4. **Run the application:**
After building the images (with or without the `IS_FIRST_DEPLOYMENT` arg as per your choice), start the services:
```bash
docker compose up
```
The frontend should typically be available at `http://localhost:4200` (or as configured) and the backend API at `http://localhost:8080`.
### Option 2: Manual Setup (for Development and Customization)
Follow these steps if you prefer to run the frontend and backend services manually on your local machine.
**A. Backend Setup**
1. **Navigate to the `backend/` directory.**
```bash
cd backend
```
2. **Create a virtual environment and install dependencies:**
```bash
# Check if you are already in an environment
pip -V
# If not, create and activate (for Linux/macOS)
python3 -m venv .venv
source .venv/bin/activate
# Install requirements
pip3 install -r requirements.txt
```
> **VS Code Tip:** If VS Code doesn't recognize your virtual environment, press `Ctrl + Shift + P` (or `Cmd + Shift + P` on Mac), type "Python: Select Interpreter", choose "Enter interpreter path...", and then find and select `.venv/bin/python` inside your `backend` directory.
3. **Setup Google Cloud (`gcloud`) credentials:**
Ensure you're authenticated and your project is configured correctly.
```bash
gcloud auth login # Login with your user account
gcloud config set project <your-project-id>
# For services using Application Default Credentials (ADC) locally
gcloud auth application-default login
# Optionally, set a quota project for ADC if not inherited
gcloud auth application-default set-quota-project <your-project-id>
# Verify configuration
gcloud auth list
gcloud config list project
```
4. **Configure Environment Variables:**
Backend configuration is managed via environment variables. Create a `.local.env` file in the `backend/` directory (you can copy from `.local.env.example` if one exists). This file should be added to `.gitignore`.
* **For Mac/Windows (or zsh console on Linux):**
Source the variables directly (from the `backend/` directory):
```bash
. ./.local.env
```
* **For Linux (bash):**
Open `backend/.venv/bin/activate` and append the `export` commands from your `backend/.local.env` file after the `PATH` export section. For example:
```sh
# ... existing activate script content ...
_OLD_VIRTUAL_PATH="$PATH"
PATH="$VIRTUAL_ENV/bin:$PATH"
export PATH
# Quickbot env variables (copied from .local.env)
export ENVIRONMENT="development"
export FRONTEND_URL="http://localhost:4200"
export GCP_PROJECT_ID="<your-project-id>"
export BIG_QUERY_DATASET="your_bq_dataset_for_search_tuning"
# ... other necessary variables for datastore, engine, etc. ...
```
Verify the variables are set by running `env` in your activated terminal.
5. **Run the setup script (if applicable):**
This script might perform initial configurations like database setup.
```bash
# from the backend/ directory
python3 setup.py
```
6. **Run the backend application:**
```bash
# from the backend/ directory
uvicorn main:app --reload --port 8080
```
**B. Frontend Setup**
(These instructions assume a typical TypeScript/Angular frontend. Adjust as necessary based on your `frontend/README.md`.)
1. **Navigate to the `frontend/` directory.**
```bash
cd frontend
```
2. **Install dependencies:**
```bash
npm install
```
3. **Environment Variables (if applicable):**
The frontend might require its own environment configuration (e.g., via a `.env` file or Angular's `environment.ts` files for API endpoints). Check the `frontend/` directory or its `README.md` for specific instructions.
4. **Run the frontend application:**
```bash
npm start
# Or, for many Angular projects:
# ng serve
```
The application will typically be available at `http://localhost:4200`.
## Project Structure (highlighting important parts)
```text
document-search-using-agent-builder/
├── backend/ # Python backend (FastAPI/Uvicorn)
│ ├── .venv/ # Python virtual environment (gitignored)
│ ├── .local.env # Local environment variables (gitignored)
│ ├── main.py # Main application file
│ ├── requirements.txt # Backend dependencies
│ ├── setup.py # Backend setup script
│ └── README.md # Backend-specific instructions
├── frontend/ # TypeScript frontend (Angular)
│ ├── node_modules/ # Node.js dependencies (gitignored)
│ ├── src/ # Frontend source code
│ ├── package.json # Frontend dependencies and scripts
│ ├── tsconfig.json # TypeScript configuration
│ └── README.md # Frontend-specific instructions
├── docker-compose.yml # Docker Compose configuration for all services
└── README.md # This file: Root project README
```
## Environment Variables
Configuration for both frontend and backend is primarily managed through environment variables.
* **Backend:**
* When running manually, backend environment variables are typically defined in `backend/.local.env`.
* When running with Docker, these variables are usually passed into the backend container via the `docker-compose.yml` file (often referencing a `.env` file at the root or `backend/` directory).
* Key variables include:
* `GCP_PROJECT_ID`: Your Google Cloud Project ID.
* `ENVIRONMENT`: Application environment (e.g., `development`, `production`).
* `FRONTEND_URL`: URL of the frontend application (e.g., `http://localhost:4200`).
* `BIG_QUERY_DATASET`: (If used) Name of the BigQuery dataset for search tuning analytics.
* Variables for Vertex AI Search: `DATA_STORE_ID`, `ENGINE_ID`, `LOCATION_ID`, etc., especially if not using the `IS_FIRST_DEPLOYMENT="True"` Docker build arg for automatic setup.
* Consult `backend/README.md` or `backend/.local.env.example` for a complete list.
* **Frontend:**
* Frontend environment variables (e.g., API endpoint URLs) are usually managed within the frontend's build system (e.g., Angular's `environment.ts` files or a `.env` file in the `frontend/` directory).
* Consult `frontend/README.md` for specific details.
## Code Styling & Commit Guidelines
To maintain code quality and consistency across the project:
* **TypeScript (Frontend):** We follow the Angular Coding Style Guide by leveraging Google's TypeScript Style Guide using `gts`. This includes a formatter, linter, and automatic code fixer.
* **Python (Backend):** We adhere to the Google Python Style Guide, using tools like `pylint` and `black` for linting and formatting.
* **Commit Messages:** We suggest following Angular's Commit Message Guidelines to create clear and descriptive commit messages.
### Frontend (TypeScript with `gts`)
(Assumes setup within the `frontend/` directory)
1. **Initialize `gts` (if not already done in the project):**
Navigate to `frontend/` and run:
```bash
npx gts init
```
This will set up `gts` and create necessary configuration files (like `tsconfig.json`). Ensure your `tsconfig.json` (or a related `gts` config file like `.gtsrc`) includes an extension for `gts` defaults, typically:
```json
{
"extends": "./node_modules/gts/tsconfig-google.json"
// ... other configurations
}
```
2. **Check for linting issues:**
(This assumes a `lint` script is defined in `frontend/package.json`, e.g., `"lint": "gts lint"`)
```bash
# from frontend/ directory
npm run lint
```
3. **Fix linting issues automatically (where possible):**
(This assumes a `fix` script is defined in `frontend/package.json`, e.g., `"fix": "gts fix"`)
```bash
# from frontend/ directory
npm run fix
```
### Backend (Python with `pylint` and `black`)
(Assumes setup within the `backend/` directory and its virtual environment activated)
1. **Ensure Dependencies are Installed:**
Add `pylint` and `black` to your `backend/requirements.txt` file if not already present:
```
pylint
black
```
Then install them within your virtual environment:
```bash
# from backend/ directory, with .venv activated
pip install pylint black
# or pip install -r requirements.txt
```
2. **Configure `pylint`:**
It's recommended to have a `.pylintrc` file in your `backend/` directory to configure `pylint` rules. You can generate one if it doesn't exist:
```bash
# from backend/ directory
pylint --generate-rcfile > .pylintrc
```
Customize this file according to your project's needs and the Google Python Style Guide.
3. **Check for linting issues with `pylint`:**
Navigate to the `backend/` directory and run:
```bash
# from backend/ directory
pylint .
# Or specify modules/packages: pylint your_module_name
```
4. **Format code with `black`:**
To automatically format all Python files in the `backend/` directory and its subdirectories:
```bash
# from backend/ directory
python -m black . --line-length=80
```
Binary file not shown.

After

Width:  |  Height:  |  Size: 769 KiB

@@ -0,0 +1,20 @@
# This file specifies files that are *not* uploaded to Google Cloud
# using gcloud. It follows the same syntax as .gitignore, with the addition of
# "#!include" directives (which insert the entries of the given .gitignore-style
# file at that point).
#
# For more information, run:
# $ gcloud topic gcloudignore
#
.gcloudignore
# If you would like to upload your .git directory, .gitignore file or files
# from your .gitignore file, remove the corresponding line
# below:
.git
.gitignore
# Python pycache:
__pycache__/
# Ignored by the build system
/setup.cfg
my_env
@@ -0,0 +1,7 @@
__pycache__/
.idea/
.vscode/
.venv/
env
bin
lib
@@ -0,0 +1,28 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
FROM python:3.11-alpine
COPY requirements.txt /tmp
RUN pip install -r /tmp/requirements.txt
WORKDIR /app
COPY . /app/
ENV ENVIRONMENT="development"
ENV BIG_QUERY_DATASET=""
ENV FRONTEND_URL=""
EXPOSE 8080
ENTRYPOINT ["gunicorn", "main:app", "--workers=4", "--worker-class=uvicorn.workers.UvicornWorker", "--timeout=36000", "--bind=0.0.0.0:8080"]
@@ -0,0 +1,92 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# syntax=docker/dockerfile:1.4
FROM python:3.11 AS builder
# Build arguments
ARG IS_FIRST_DEPLOYMENT="False"
ARG GCLOUD_PROJECT=""
ARG ENVIRONMENT="development"
ARG FRONTEND_URL=""
ARG BIG_QUERY_DATASET=""
ARG VERTEX_AI_LOCATION=""
ARG VERTEX_AI_DATASTORE_ID=""
ARG VERTEX_AI_ENGINE_ID=""
# Environment variables for the build process
ENV IS_FIRST_DEPLOYMENT=$IS_FIRST_DEPLOYMENT
ENV GCLOUD_PROJECT=$GCLOUD_PROJECT
ENV ENVIRONMENT=$ENVIRONMENT
ENV FRONTEND_URL=$FRONTEND_URL
ENV BIG_QUERY_DATASET=$BIG_QUERY_DATASET
ENV VERTEX_AI_LOCATION=$VERTEX_AI_LOCATION
ENV VERTEX_AI_DATASTORE_ID=$VERTEX_AI_DATASTORE_ID
ENV VERTEX_AI_ENGINE_ID=$VERTEX_AI_ENGINE_ID
WORKDIR /code
COPY . .
COPY setup.py ./setup.py
RUN pip install --no-cache-dir -r /code/requirements.txt
RUN --mount=type=secret,id=gcp_credentials,target=/tmp/gcp_adc.json,required=true \
if [ "$IS_FIRST_DEPLOYMENT" = "True" ]; then \
echo "--- Running first deployment setup (IS_FIRST_DEPLOYMENT=True) ---"; \
echo "Using GCLOUD_PROJECT=${GCLOUD_PROJECT} for setup"; \
\
if [ ! -f /tmp/gcp_adc.json ]; then \
echo "CRITICAL ERROR: GCP credentials secret was expected but not mounted to /tmp/gcp_adc.json" >&2; \
exit 1; \
fi; \
echo "DEBUG: GCP credentials secret successfully mounted to /tmp/gcp_adc.json"; \
\
# Set GOOGLE_APPLICATION_CREDENTIALS specifically for this RUN command's execution context
export GOOGLE_APPLICATION_CREDENTIALS=/tmp/gcp_adc.json; \
echo "DEBUG: GOOGLE_APPLICATION_CREDENTIALS for setup is set to: $GOOGLE_APPLICATION_CREDENTIALS"; \
\
echo "Executing setup.py ..."; \
# Now setup.py can make requests to GCP with provided credentials
python /code/setup.py || { echo "setup.py with failed!"; exit 1; }; \
echo "--- First deployment setup finished ---"; \
else \
echo "--- IS_FIRST_DEPLOYMENT is False, skipping first deployment setup. ---"; \
fi
FROM python:3.11-slim AS final
ARG GCLOUD_PROJECT=""
ARG GOOGLE_APPLICATION_CREDENTIALS=""
ARG ENVIRONMENT="development"
ARG FRONTEND_URL=""
ARG BIG_QUERY_DATASET=""
# Environment variables for the build process
ENV GCLOUD_PROJECT=$GCLOUD_PROJECT
ENV GOOGLE_APPLICATION_CREDENTIALS="/root/.config/gcloud/application_default_credentials.json"
ENV ENVIRONMENT=$ENVIRONMENT
ENV FRONTEND_URL=$FRONTEND_URL
ENV BIG_QUERY_DATASET=$BIG_QUERY_DATASET
WORKDIR /app
COPY --from=builder /code/requirements.txt /app/requirements.txt
RUN pip install --no-cache-dir -r /app/requirements.txt
COPY --from=builder /code/main.py /app/main.py
COPY --from=builder /code/src /app/src
EXPOSE 8080
ENTRYPOINT ["gunicorn", "main:app", "--workers=4", "--worker-class=uvicorn.workers.UvicornWorker", "--timeout=36000", "--bind=0.0.0.0:8080"]
@@ -0,0 +1,132 @@
# QuickBotApp document-search-using-agent-builder
## Setting up
### 1. Create virtualenv and install dependencies
Create a virtual environment on the root of the application, activate it and install the requirements
```
# check if you are already in the env
pip -V
# if not then
python3 -m venv .venv
source .venv/bin/activate
pip3 install -r requirements.txt
```
> **IMPORTANT!** VS Code may not recognize your env, in that case type "ctrl + shift + P", then select "Python: Select Interpreter" and then select "Enter interpreter path..." and then select your .venv python interpreter, in this case .backend/.venv/bin/python
### 2. Setup gcloud credentials
```
gcloud auth list
gcloud config list
gcloud auth login
gcloud config set project <your project id>
gcloud auth application-default set-quota-project <your project id>
gcloud auth list
gcloud config list
```
### 3. Add environment variables
#### If you have Mac or Windows (or if you are using zsh console on Linux)
```
. ./local.env
```
#### If you have Linux
Open the file .venv/bin/activate and paste the env variables from `.local.env` after the PATH export, like this:
```
...
_OLD_VIRTUAL_PATH="$PATH"
PATH="$VIRTUAL_ENV/bin:$PATH"
export PATH
# Quickbot env variables
export ENVIRONMENT=development
export FRONTEND_URL=http://localhost:4200
export BIG_QUERY_DATASET=eren
...
```
Check that the env variables has been taken into account, running:
```
env
```
You should see the new env variables set there
### 4. Running the set up script
```
python3 setup.py
```
### 5. Run the application
Finally run using uvicorn
```
uvicorn main:app --reload --port 8080
```
## Code Styling & Commit Guidelines
To maintain code quality and consistency:
* **TypeScript (Frontend):** We follow [Angular Coding Style Guide](https://angular.dev/style-guide) by leveraging the use of [Google's TypeScript Style Guide](https://github.com/google/gts) using `gts`. This includes a formatter, linter, and automatic code fixer.
* **Python (Backend):** We adhere to the [Google Python Style Guide](https://google.github.io/styleguide/pyguide.html), using tools like `pylint` and `black` for linting and formatting.
* **Commit Messages:** We suggest following [Angular's Commit Message Guidelines](https://github.com/angular/angular/blob/main/contributing-docs/commit-message-guidelines.md) to create clear and descriptive commit messages.
#### Frontend (TypeScript with `gts`)
1. **Initialize `gts` (if not already done in the project):**
Navigate to the `frontend/` directory and run:
```bash
npx gts init
```
This will set up `gts` and create necessary configuration files (like `tsconfig.json`). Ensure your `tsconfig.json` (or a related gts config file like `.gtsrc`) includes an extension for `gts` defaults, typically:
```json
{
"extends": "./node_modules/gts/tsconfig-google.json",
// ... other configurations
}
```
2. **Check for linting issues:**
```bash
npm run lint
```
(This assumes a `lint` script is defined in `package.json`, e.g., `"lint": "gts lint"`)
3. **Fix linting issues automatically (where possible):**
```bash
npm run fix
```
(This assumes a `fix` script is defined in `package.json`, e.g., `"fix": "gts fix"`)
#### Backend (Python with `pylint` and `black`)
1. **Ensure Dependencies are Installed:**
Add `pylint` and `black` to your `backend/requirements.txt` file:
```
pylint
black
```
Then install them within your virtual environment:
```bash
pip install pylint black
# or pip install -r requirements.txt
```
2. **Configure `pylint`:**
It's recommended to have a `.pylintrc` file in your `backend/` directory to configure `pylint` rules. You might need to copy a standard one or generate one (`pylint --generate-rcfile > .pylintrc`).
3. **Check for linting issues with `pylint`:**
Navigate to the `backend/` directory and run:
```bash
pylint .
```
(Or specify modules/packages: `pylint your_module_name`)
4. **Format code with `black`:**
To automatically format all Python files in the current directory and subdirectories:
```bash
python -m black . --line-length=80
```
@@ -0,0 +1,14 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
@@ -0,0 +1,3 @@
export ENVIRONMENT=development
export FRONTEND_URL=http://localhost:4200
export BIG_QUERY_DATASET=eren
@@ -0,0 +1,104 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Main FastAPI application entry point for the Quick Bot Backend.
This module initializes the FastAPI application, configures CORS based on
the environment, defines root and version endpoints, includes API routers
(e.g., for search functionality), and provides an endpoint for audio
transcription using Google Cloud Speech-to-Text.
"""
from fastapi import FastAPI, File, UploadFile
from fastapi.middleware.cors import CORSMiddleware
from src.controller.search import router as search_router
from google.cloud import speech
from os import getenv
app = FastAPI()
def configure_cors(app):
"""Configures CORS middleware based on the environment."""
environment = getenv("ENVIRONMENT")
allowed_origins = []
if environment == "production":
frontend_url = getenv("FRONTEND_URL")
if not frontend_url:
raise ValueError(
"FRONTEND_URL environment variable not set in production"
)
allowed_origins.append(frontend_url)
elif environment == "development":
allowed_origins.append("*") # Allow all origins in development
else:
raise ValueError(
f"""Invalid ENVIRONMENT: {environment}.
Must be 'production' or 'development'"""
)
app.add_middleware(
CORSMiddleware,
allow_origins=allowed_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Create a route to handle GET requests on root
@app.get("/")
async def root():
return "You are calling Quick Bot Backend"
# Create a route to handle GET requests on /version
@app.get("/api/version")
def version():
return "v0.0.1"
@app.post("/api/audio_chat")
async def audio_chat(audio_file: UploadFile = File(...)):
client = speech.SpeechClient()
audio_content = await audio_file.read()
audio = speech.RecognitionAudio(content=audio_content)
config = speech.RecognitionConfig(
language_code="en-US",
sample_rate_hertz=48000,
model="default",
audio_channel_count=1,
enable_word_confidence=True,
enable_word_time_offsets=True,
)
operation = client.long_running_recognize(config=config, audio=audio)
print("Waiting for operation to complete...")
response = operation.result(timeout=90)
print(response)
text = ""
for result in response.results:
print(f"Transcript: {result.alternatives[0].transcript}")
text = result.alternatives[0].transcript
return text, 200
configure_cors(app)
app.include_router(search_router)
@@ -0,0 +1,399 @@
# This Pylint rcfile contains a best-effort configuration to uphold the
# best-practices and style described in the Google Python style guide:
# https://google.github.io/styleguide/pyguide.html
#
# Its canonical open-source location is:
# https://google.github.io/styleguide/pylintrc
[MAIN]
# Files or directories to be skipped. They should be base names, not paths.
ignore=third_party
# Files or directories matching the regex patterns are skipped. The regex
# matches against base names, not paths.
ignore-patterns=
# Pickle collected data for later comparisons.
persistent=no
# List of plugins (as comma separated values of python modules names) to load,
# usually to register additional checkers.
load-plugins=
# Use multiple processes to speed up Pylint.
jobs=4
# Allow loading of arbitrary C extensions. Extensions are imported into the
# active Python interpreter and may run arbitrary code.
unsafe-load-any-extension=no
[MESSAGES CONTROL]
# Only show warnings with the listed confidence levels. Leave empty to show
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
confidence=
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
# multiple time (only on the command line, not in the configuration file where
# it should appear only once). See also the "--disable" option for examples.
#enable=
# Disable the message, report, category or checker with the given id(s). You
# can either give multiple identifiers separated by comma (,) or put this
# option multiple times (only on the command line, not in the configuration
# file where it should appear only once).You can also use "--disable=all" to
# disable everything first and then reenable specific checks. For example, if
# you want to run only the similarities checker, you can use "--disable=all
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use"--disable=all --enable=classes
# --disable=W"
disable=R,
abstract-method,
apply-builtin,
arguments-differ,
attribute-defined-outside-init,
backtick,
bad-option-value,
basestring-builtin,
buffer-builtin,
c-extension-no-member,
consider-using-enumerate,
cmp-builtin,
cmp-method,
coerce-builtin,
coerce-method,
delslice-method,
div-method,
eq-without-hash,
execfile-builtin,
file-builtin,
filter-builtin-not-iterating,
fixme,
getslice-method,
global-statement,
hex-method,
idiv-method,
implicit-str-concat,
import-error,
import-self,
import-star-module-level,
input-builtin,
intern-builtin,
invalid-str-codec,
locally-disabled,
long-builtin,
long-suffix,
map-builtin-not-iterating,
misplaced-comparison-constant,
missing-function-docstring,
metaclass-assignment,
next-method-called,
next-method-defined,
no-absolute-import,
no-init, # added
no-member,
no-name-in-module,
no-self-use,
nonzero-method,
oct-method,
old-division,
old-ne-operator,
old-octal-literal,
old-raise-syntax,
parameter-unpacking,
print-statement,
raising-string,
range-builtin-not-iterating,
raw_input-builtin,
rdiv-method,
reduce-builtin,
relative-import,
reload-builtin,
round-builtin,
setslice-method,
signature-differs,
standarderror-builtin,
suppressed-message,
sys-max-int,
trailing-newlines,
unichr-builtin,
unicode-builtin,
unnecessary-pass,
unpacking-in-except,
useless-else-on-loop,
useless-suppression,
using-cmp-argument,
wrong-import-order,
xrange-builtin,
zip-builtin-not-iterating,
[REPORTS]
# Set the output format. Available formats are text, parseable, colorized, msvs
# (visual studio) and html. You can also give a reporter class, eg
# mypackage.mymodule.MyReporterClass.
output-format=text
# Tells whether to display a full report or only the messages
reports=no
# Python expression which should return a note less than 10 (10 is the highest
# note). You have access to the variables errors warning, statement which
# respectively contain the number of errors / warnings messages and the total
# number of statements analyzed. This is used by the global evaluation report
# (RP0004).
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
# Template used to display messages. This is a python new-style format string
# used to format the message information. See doc for all details
#msg-template=
[BASIC]
# Good variable names which should always be accepted, separated by a comma
good-names=main,_
# Bad variable names which should always be refused, separated by a comma
bad-names=
# Colon-delimited sets of names that determine each other's naming style when
# the name regexes allow several styles.
name-group=
# Include a hint for the correct naming format with invalid-name
include-naming-hint=no
# List of decorators that produce properties, such as abc.abstractproperty. Add
# to this list to register other decorators that produce valid properties.
property-classes=abc.abstractproperty,cached_property.cached_property,cached_property.threaded_cached_property,cached_property.cached_property_with_ttl,cached_property.threaded_cached_property_with_ttl
# Regular expression matching correct function names
function-rgx=^(?:(?P<exempt>setUp|tearDown|setUpModule|tearDownModule)|(?P<camel_case>_?[A-Z][a-zA-Z0-9]*)|(?P<snake_case>_?[a-z][a-z0-9_]*))$
# Regular expression matching correct variable names
variable-rgx=^[a-z][a-z0-9_]*$
# Regular expression matching correct constant names
const-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$
# Regular expression matching correct attribute names
attr-rgx=^_{0,2}[a-z][a-z0-9_]*$
# Regular expression matching correct argument names
argument-rgx=^[a-z][a-z0-9_]*$
# Regular expression matching correct class attribute names
class-attribute-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$
# Regular expression matching correct inline iteration names
inlinevar-rgx=^[a-z][a-z0-9_]*$
# Regular expression matching correct class names
class-rgx=^_?[A-Z][a-zA-Z0-9]*$
# Regular expression matching correct module names
module-rgx=^(_?[a-z][a-z0-9_]*|__init__)$
# Regular expression matching correct method names
method-rgx=(?x)^(?:(?P<exempt>_[a-z0-9_]+__|runTest|setUp|tearDown|setUpTestCase|tearDownTestCase|setupSelf|tearDownClass|setUpClass|(test|assert)_*[A-Z0-9][a-zA-Z0-9_]*|next)|(?P<camel_case>_{0,2}[A-Z][a-zA-Z0-9_]*)|(?P<snake_case>_{0,2}[a-z][a-z0-9_]*))$
# Regular expression which should only match function or class names that do
# not require a docstring.
no-docstring-rgx=(__.*__|main|test.*|.*test|.*Test)$
# Minimum line length for functions/classes that require docstrings, shorter
# ones are exempt.
docstring-min-length=12
[TYPECHECK]
# List of decorators that produce context managers, such as
# contextlib.contextmanager. Add to this list to register other decorators that
# produce valid context managers.
contextmanager-decorators=contextlib.contextmanager,contextlib2.contextmanager
# List of module names for which member attributes should not be checked
# (useful for modules/projects where namespaces are manipulated during runtime
# and thus existing member attributes cannot be deduced by static analysis. It
# supports qualified module names, as well as Unix pattern matching.
ignored-modules=
# List of class names for which member attributes should not be checked (useful
# for classes with dynamically set attributes). This supports the use of
# qualified names.
ignored-classes=optparse.Values,thread._local,_thread._local
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E1101 when accessed. Python regular
# expressions are accepted.
generated-members=
[FORMAT]
# Maximum number of characters on a single line.
max-line-length=80
# TODO(https://github.com/pylint-dev/pylint/issues/3352): Direct pylint to exempt
# lines made too long by directives to pytype.
# Regexp for a line that is allowed to be longer than the limit.
ignore-long-lines=(?x)(
^\s*(\#\ )?<?https?://\S+>?$|
^\s*(from\s+\S+\s+)?import\s+.+$)
# Allow the body of an if to be on the same line as the test if there is no
# else.
single-line-if-stmt=yes
# Maximum number of lines in a module
max-module-lines=99999
# String used as indentation unit. The internal Google style guide mandates 2
# spaces. Google's externaly-published style guide says 4, consistent with
# PEP 8. Here, we use 2 spaces, for conformity with many open-sourced Google
# projects (like TensorFlow).
indent-string=' '
# Number of spaces of indent required inside a hanging or continued line.
indent-after-paren=4
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
expected-line-ending-format=
[MISCELLANEOUS]
# List of note tags to take in consideration, separated by a comma.
notes=TODO
[STRING]
# This flag controls whether inconsistent-quotes generates a warning when the
# character used as a quote delimiter is used inconsistently within a module.
check-quote-consistency=yes
[VARIABLES]
# Tells whether we should check for unused import in __init__ files.
init-import=no
# A regular expression matching the name of dummy variables (i.e. expectedly
# not used).
dummy-variables-rgx=^\*{0,2}(_$|unused_|dummy_)
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid to define new builtins when possible.
additional-builtins=
# List of strings which can identify a callback function by name. A callback
# name must start or end with one of those strings.
callbacks=cb_,_cb
# List of qualified module names which can have objects that can redefine
# builtins.
redefining-builtins-modules=six,six.moves,past.builtins,future.builtins,functools
[LOGGING]
# Logging modules to check that the string format arguments are in logging
# function parameter format
logging-modules=logging,absl.logging,tensorflow.io.logging
[SIMILARITIES]
# Minimum lines number of a similarity.
min-similarity-lines=4
# Ignore comments when computing similarities.
ignore-comments=yes
# Ignore docstrings when computing similarities.
ignore-docstrings=yes
# Ignore imports when computing similarities.
ignore-imports=no
[SPELLING]
# Spelling dictionary name. Available dictionaries: none. To make it working
# install python-enchant package.
spelling-dict=
# List of comma separated words that should not be checked.
spelling-ignore-words=
# A path to a file that contains private dictionary; one word per line.
spelling-private-dict-file=
# Tells whether to store unknown words to indicated private dictionary in
# --spelling-private-dict-file option instead of raising a message.
spelling-store-unknown-words=no
[IMPORTS]
# Deprecated modules which should not be used, separated by a comma
deprecated-modules=regsub,
TERMIOS,
Bastion,
rexec,
sets
# Create a graph of every (i.e. internal and external) dependencies in the
# given file (report RP0402 must not be disabled)
import-graph=
# Create a graph of external dependencies in the given file (report RP0402 must
# not be disabled)
ext-import-graph=
# Create a graph of internal dependencies in the given file (report RP0402 must
# not be disabled)
int-import-graph=
# Force import order to recognize a module as part of the standard
# compatibility libraries.
known-standard-library=
# Force import order to recognize a module as part of a third party library.
known-third-party=enchant, absl
# Analyse import fallback blocks. This can be used to support both Python 2 and
# 3 compatible code, which means that the block might have code that exists
# only in one or another interpreter, leading to false positives when analysed.
analyse-fallback-blocks=no
[CLASSES]
# List of method names used to declare (i.e. assign) instance attributes.
defining-attr-methods=__init__,
__new__,
setUp
# List of member names, which should be excluded from the protected access
# warning.
exclude-protected=_asdict,
_fields,
_replace,
_source,
_make
# List of valid names for the first argument in a class method.
valid-classmethod-first-arg=cls,
class_
# List of valid names for the first argument in a metaclass class method.
valid-metaclass-classmethod-first-arg=mcs
@@ -0,0 +1,30 @@
gunicorn==20.1.0; python_version > '3.0'
gunicorn==19.10.0; python_version < '3.0'
uvicorn~=0.17.0
fastapi~=0.111.1
fastapi-utilities==0.2.0
cloudpickle==2.2.1
grpcio==1.66.2
urllib3==1.26.16
typing-inspect==0.9.0
typing_extensions==4.12.2
pydantic==2.9.2
requests==2.31.0
langchain==0.3.2
langchain-community==0.3.1
langchain-core==0.3.9
langchain-google-vertexai==2.0.4
google-cloud-aiplatform==1.69.0
google-cloud-bigquery==3.26.0
google-cloud-tasks==2.16.5
google-cloud-logging==3.11.2
google-cloud-speech==2.27.0
google-cloud-discoveryengine==0.13.4
pylint
black
pytest
pytest-cov
pytest-watch
@@ -0,0 +1,14 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
@@ -0,0 +1,69 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Utility functions for setting up Google BigQuery datasets and tables."""
from typing import List
from google.cloud.bigquery import Client as BigQueryClient, Table, SchemaField, TableReference
bigquery_client = BigQueryClient()
PROJECT_ID = bigquery_client.project
def create_dataset(dataset_name: str):
"""Deletes an existing BigQuery dataset and recreates it.
If the dataset already exists, it and all its contents
will be deleted first.
Then, a new empty dataset with the specified name is created.
Args:
dataset_name: The name for the BigQuery dataset.
"""
dataset_id = f"{PROJECT_ID}.{dataset_name}"
try:
bigquery_client.delete_dataset(
dataset_id, delete_contents=True, not_found_ok=True
)
print(f"Dataset {dataset_id} deleted (if it existed).")
except Exception as e:
print(f"Error deleting dataset {dataset_id}: {e}")
print(f"Creating dataset {dataset_id}...")
bigquery_client.create_dataset(dataset_name, exists_ok=True) # exists_ok=True in case delete failed but it exists
print(f"Dataset {dataset_id} ensured.")
def create_table(dataset: str, table_name: str, schema: List[SchemaField]):
"""Creates a BigQuery table within a specified dataset.
If the table already exists, it will be deleted and recreated.
Args:
dataset: The name of the dataset where the table will be created.
table_name: The name for the new BigQuery table.
schema: A list of SchemaField objects defining the table's structure.
"""
table_id_full = f"{PROJECT_ID}.{dataset}.{table_name}"
table_ref = TableReference.from_string(table_id_full)
try:
bigquery_client.delete_table(table_ref, not_found_ok=True)
print(f"Table {table_id_full} deleted (if it existed).")
except Exception as e:
print(f"Notice: Could not delete table {table_id_full} (may not exist or other issue): {e}")
print(f"Creating table {table_id_full}...")
bq_table = Table(table_ref, schema=schema)
bigquery_client.create_table(bq_table)
print(f"Table {table_id_full} created successfully.")
@@ -0,0 +1,143 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from google.cloud import discoveryengine_v1 as discoveryengine
from google.api_core.exceptions import AlreadyExists, NotFound
def create_vertex_ai_datastore(
project_id: str,
location: str,
datastore_id: str,
datastore_display_name: str,
):
"""Creates a Vertex AI Search Datastore if it doesn't exist."""
client = discoveryengine.DataStoreServiceClient()
parent = client.collection_path(project_id, location, "default_collection")
datastore_name_full = client.data_store_path(project_id, location, datastore_id)
try:
ds = client.get_data_store(name=datastore_name_full)
print(f"Datastore '{datastore_id}' already exists in location '{location}': {ds.name}")
return ds
except NotFound:
print(f"Datastore '{datastore_id}' not found in location '{location}'. Attempting to create...")
datastore = discoveryengine.DataStore(
display_name=datastore_display_name,
industry_vertical=discoveryengine.IndustryVertical.GENERIC,
content_config=discoveryengine.DataStore.ContentConfig.CONTENT_REQUIRED,
)
try:
operation = client.create_data_store(
parent=parent,
data_store=datastore,
data_store_id=datastore_id,
)
print(f"Waiting for Datastore '{datastore_id}' creation (LRO: {operation.operation.name})...")
created_datastore = operation.result(timeout=300) # 5 minutes timeout
print(f"Successfully created Datastore: {created_datastore.name}")
return created_datastore
except AlreadyExists:
print(f"Datastore '{datastore_id}' creation reported AlreadyExists. Fetching existing.")
return client.get_data_store(name=datastore_name_full)
except Exception as e:
print(f"Error creating Datastore '{datastore_id}': {e}")
raise
def import_documents_to_datastore(
project_id: str,
location: str,
datastore_id: str,
gcs_uri: str,
):
"""Imports documents from GCS into the specified Datastore."""
client = discoveryengine.DocumentServiceClient()
parent_branch = client.branch_path(
project=project_id,
location=location,
data_store=datastore_id,
branch="default_branch",
)
request = discoveryengine.ImportDocumentsRequest(
parent=parent_branch,
gcs_source=discoveryengine.GcsSource(input_uris=[gcs_uri], data_schema="content"),
reconciliation_mode=discoveryengine.ImportDocumentsRequest.ReconciliationMode.INCREMENTAL
)
try:
print(f"Starting document import from '{gcs_uri}' into Datastore '{datastore_id}'...")
operation = client.import_documents(request=request)
print(f"Waiting for document import to complete (LRO: {operation.operation.name}). This may take several minutes...")
response = operation.result(timeout=1800) # 30 minutes timeout
if response.error_samples and len(response.error_samples) > 0:
print(f"Document import completed with errors. Error Config {response.error_config}")
for i, error_sample in enumerate(response.error_samples):
print(f" Error sample {i+1}: {error_sample.message}")
raise Exception("Document import failed with errors", response)
else:
print(f"Successfully imported documents.")
return response
except Exception as e:
print(f"Error during document import for Datastore '{datastore_id}': {e}")
raise
def create_vertex_ai_engine(
project_id: str,
location: str,
engine_id: str,
engine_display_name: str,
datastore_ids_list: list[str],
):
"""Creates a Vertex AI Search Engine (App) if it doesn't exist."""
client = discoveryengine.EngineServiceClient()
parent_collection = client.collection_path(project_id, location, "default_collection")
engine_name_full = client.engine_path(project_id, location, "default_collection", engine_id)
try:
eng = client.get_engine(name=engine_name_full)
print(f"Engine '{engine_id}' already exists in location '{location}': {eng.name}")
return eng
except NotFound:
print(f"Engine '{engine_id}' not found in location '{location}'. Attempting to create...")
engine_config = discoveryengine.Engine(
display_name=engine_display_name,
solution_type=discoveryengine.SolutionType.SOLUTION_TYPE_SEARCH,
data_store_ids=datastore_ids_list,
common_config=discoveryengine.Engine.CommonConfig(company_name="QuickBot App"),
search_engine_config=discoveryengine.Engine.SearchEngineConfig(
search_tier="SEARCH_TIER_STANDARD",
search_add_ons=["SEARCH_ADD_ON_LLM"]
)
)
try:
operation = client.create_engine(
parent=parent_collection,
engine=engine_config,
engine_id=engine_id,
)
print(f"Waiting for Engine '{engine_id}' creation (LRO: {operation.operation.name})...")
created_engine = operation.result(timeout=600) # 10 minutes timeout
print(f"Successfully created Engine: {created_engine.name}")
return created_engine
except AlreadyExists:
print(f"Engine '{engine_id}' creation reported AlreadyExists. Fetching existing.")
return client.get_engine(name=engine_name_full)
except Exception as e:
print(f"Error creating Engine '{engine_id}': {e}")
raise
@@ -0,0 +1,120 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Initializes the BigQuery and Vertex AI Search environment for the application.
This script performs the following actions:
1. Retrieves configuration from environment variables or uses defaults.
2. Creates the specified BigQuery dataset if it doesn't already exist.
3. Creates the 'search_applications' table within that dataset.
4. Creates a Vertex AI Search Datastore if it doesn't already exist.
5. Imports documents from a specified GCS bucket into the Datastore.
6. Creates a Vertex AI Search Engine (App) linked to the Datastore.
Usage:
Run this script directly (e.g., `python setup.py`).
Set environment variables to override defaults:
- 'BIG_QUERY_DATASET'
- 'GOOGLE_CLOUD_PROJECT'
- 'VERTEX_AI_SEARCH_LOCATION'
- 'VERTEX_AI_DATASTORE_ID'
- 'VERTEX_AI_ENGINE_ID'
"""
from os import getenv
from scripts.big_query_setup import create_dataset, create_table
from src.service.search_application import SEARCH_APPLICATION_TABLE
from src.model.search import SearchApplication
from scripts.vertexai_search_setup import create_vertex_ai_datastore, create_vertex_ai_engine, import_documents_to_datastore
def main():
# 1. BigQuery Setup
print("--- Setting up BigQuery ---")
BIG_QUERY_DATASET = getenv("BIG_QUERY_DATASET", "quickbot_default_bq_dataset")
GCLOUD_PROJECT = getenv("GCLOUD_PROJECT", "my-gcloud-project")
create_dataset(BIG_QUERY_DATASET)
create_table(
BIG_QUERY_DATASET, SEARCH_APPLICATION_TABLE, SearchApplication.__schema__()
)
# 2. Vertex AI Search Setup
print("--- Setting up Vertex AI Search ---")
VERTEX_AI_LOCATION = getenv("VERTEX_AI_LOCATION", "global")
VERTEX_AI_DATASTORE_ID = getenv("VERTEX_AI_DATASTORE_ID", "quickbot_alphabet_pdfs_ds")
VERTEX_AI_ENGINE_ID = getenv("VERTEX_AI_ENGINE_ID", "quickbot_alphabet_search_engine")
GCS_SOURCE_URI = "gs://cloud-samples-data/gen-app-builder/search/alphabet-investor-pdfs/*.pdf"
DATASTORE_DISPLAY_NAME_PREFIX = "Alphabet Investor Docs DS"
ENGINE_DISPLAY_NAME_PREFIX = "Alphabet Investor Engine"
datastore_display_name = f"{DATASTORE_DISPLAY_NAME_PREFIX} ({VERTEX_AI_DATASTORE_ID})"
engine_display_name = f"{ENGINE_DISPLAY_NAME_PREFIX} ({VERTEX_AI_ENGINE_ID})"
try:
# Create/Get Datastore
print(f"Attempting to create/get Datastore '{VERTEX_AI_DATASTORE_ID}' in project '{GCLOUD_PROJECT}' location '{VERTEX_AI_LOCATION}'...")
datastore = create_vertex_ai_datastore(
GCLOUD_PROJECT, VERTEX_AI_LOCATION, VERTEX_AI_DATASTORE_ID, datastore_display_name
)
if not datastore:
print("Datastore creation/retrieval failed. Aborting further Vertex AI Search setup.")
print("--- Application setup finished (with errors) ---")
raise
print(f"Successfully ensured Datastore exists: {datastore.name}")
# Import documents into Datastore
# Note: This will attempt to import documents every time the script runs.
# For production, you might want to add a check to skip this if documents
# are already present or if a previous import was successful.
print(f"\nAttempting to import documents from '{GCS_SOURCE_URI}' into datastore: {datastore.name}")
import_documents_to_datastore(
GCLOUD_PROJECT, VERTEX_AI_LOCATION, VERTEX_AI_DATASTORE_ID, GCS_SOURCE_URI
)
# Note: Document import can take a long time. The script waits.
print("Document import process initiated/completed.\n")
# Create/Get Engine
print(f"Attempting to create/get Engine '{VERTEX_AI_ENGINE_ID}' in project '{GCLOUD_PROJECT}' location '{VERTEX_AI_LOCATION}'...")
# The create_vertex_ai_engine function expects a list of datastore IDs (not full resource names).
engine = create_vertex_ai_engine(
GCLOUD_PROJECT,
VERTEX_AI_LOCATION,
VERTEX_AI_ENGINE_ID,
engine_display_name,
[VERTEX_AI_DATASTORE_ID] # Pass the Datastore ID string
)
if not engine:
print("Engine creation/retrieval failed.")
print("--- Application setup finished (with errors) ---")
raise
print(f"Successfully ensured Engine exists: {engine.name}")
print("\nVertex AI Search setup completed successfully.")
except Exception as e:
print(f"A critical error occurred during the setup process: {e}")
import traceback
print("Detailed traceback:")
print(traceback.format_exc())
# If running in Docker build, exiting with non-zero will fail the build
import sys
sys.exit(1)
print("\n--- Application setup finished ---")
print("\nSuccess! All resources should now be configured.\n")
if __name__ == "__main__":
main()
@@ -0,0 +1,14 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
@@ -0,0 +1,14 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
@@ -0,0 +1,153 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""API endpoints for managing and performing document searches."""
from fastapi import APIRouter, Request, HTTPException, Response
from google.cloud import storage
from pydantic import BaseModel
from src.model.http_status import BadRequest
from src.model.search import CreateSearchRequest, SearchApplication
from src.service.engine import EngineService
from src.service.search import SearchService
from src.service.search_application import SearchApplicationService
class SignedUrlRequest(BaseModel):
"""Request model for fetching a document directly from GCS."""
gcs_url: str
storage_client = storage.Client()
router = APIRouter(
prefix="/api/search",
tags=["searches"],
responses={404: {"description": "Not found"}},
)
@router.post("")
async def search(item: CreateSearchRequest):
"""
Performs a search using the configured Search Application.
Args:
item: The search request containing the search term.
Raises:
BadRequest: If no Search Application is configured for the project.
Returns:
The search results from the SearchService.
"""
service = SearchApplicationService()
search_application = service.get()
if not search_application:
raise BadRequest(detail="No Search Application found on project")
service = SearchService(
search_application,
)
return service.search(item.term)
@router.get("/engines")
async def get_all_engines():
"""Retrieves all available Search Engines."""
service = EngineService()
return service.get_all()
@router.get("/application")
async def get_search_application():
"""Retrieves the currently configured Search Application."""
service = SearchApplicationService()
return service.get()
@router.post("/application")
async def create_search_application(search_application: SearchApplication):
"""
Creates a new Search Application configuration.
Args:
search_application: The details of the Search Application to create.
Returns:
The created Search Application configuration.
"""
service = SearchApplicationService()
return service.create(search_application)
@router.put("/application/{engine_id}")
async def update_search_application(
engine_id: str, search_application: SearchApplication
):
"""
Updates an existing Search Application configuration.
Args:
engine_id: The ID of the engine associated with
the application to update.
search_application: The updated details for the Search Application.
Returns:
The updated Search Application configuration.
"""
service = SearchApplicationService()
return service.update(engine_id, search_application)
@router.post("/doc")
async def get_document(request: Request, response_model=None):
"""
Fetches a document directly from GCS and returns its content.
Expects a JSON body with a 'gcs_url' field specifying the full GCS path
(e.g., "gs://your-bucket-name/your-file.pdf").
Args:
request: The incoming FastAPI request object.
Raises:
HTTPException: If the GCS URL is invalid or the file cannot be fetched.
Returns:
A FastAPI Response object containing the raw PDF content.
"""
try:
req_body = await request.json()
signed_url_request = SignedUrlRequest(**req_body)
gcs_url = signed_url_request.gcs_url
bucket_name = gcs_url.split("/")[2]
object_name = "/".join(gcs_url.split("/")[3:])
# Get a reference to the blob (PDF file)
bucket = storage_client.bucket(bucket_name)
blob = bucket.blob(object_name)
# Download the blob content as bytes
pdf_content = blob.download_as_bytes()
# Return the PDF content with appropriate headers
return Response(content=pdf_content, media_type="application/pdf")
except Exception as e:
raise HTTPException(
status_code=500, detail=f"Error fetching PDF: {str(e)}"
)
@@ -0,0 +1,14 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
@@ -0,0 +1,53 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Defines custom HTTP exception classes for the FastAPI application.
This module provides reusable exception classes that inherit from FastAPI's
HTTPException, allowing for standardized error responses with specific
status codes and detail messages.
"""
from fastapi import HTTPException
class ResourceAlreadyExists(HTTPException):
"""
Custom exception for HTTP 400 Bad Request errors.
Used specifically when an attempt is made to create a resource that
already exists (e.g., creating a duplicate configuration).
Defaults to status code 400.
"""
def __init__(self, detail="Resource already exists"):
"""Initializes the exception with a default detail message."""
super().__init__(status_code=400, detail=detail)
class BadRequest(HTTPException):
"""
Custom exception for general HTTP 400 Bad Request errors.
Can be used for various client-side errors like invalid input,
missing required data, or violating business logic rules before
processing. Defaults to status code 400.
Note: The default detail message "Resource already exists" seems
inconsistent with a general BadRequest. Consider changing it to
a more generic message like "Bad Request" or requiring a specific
detail message upon instantiation.
"""
def __init__(self, detail="Resource already exists"):
super().__init__(status_code=400, detail=detail)
@@ -0,0 +1,119 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Defines data models for search operations and configurations.
This module includes Pydantic models for API requests/responses related to
search applications and engines, as well as dataclasses for representing
search results internally. It also handles fetching the default Google Cloud
Project ID.
"""
from dataclasses import dataclass
from typing import List, Optional
from pydantic import BaseModel
from google.cloud.bigquery import SchemaField, Row
from google.api_core.client_options import ClientOptions
import google.auth
_, PROJECT_ID = google.auth.default()
class CreateSearchRequest(BaseModel):
"""Request model for initiating a search."""
term: str
class SearchApplication(BaseModel):
"""Represents the configuration for a Discovery Engine
Search Application."""
engine_id: str
region: str
@classmethod
def __schema__(cls) -> List[SchemaField]:
"""Defines the BigQuery schema for storing SearchApplication data."""
return [
SchemaField("engine_id", "STRING", mode="REQUIRED"),
SchemaField("region", "STRING", mode="REQUIRED"),
]
@classmethod
def from_row(cls, row: Row) -> 'SearchApplication':
"""Creates a SearchApplication instance from a BigQuery Row object.
Args:
row: The BigQuery Row object. Assumes row contains fields matching the schema.
Returns:
A SearchApplication instance.
"""
# Access by field name for robustness, assuming schema matches
return cls(engine_id=row["engine_id"], region=row["region"])
def to_dict(self):
"""Converts the SearchApplication instance to a dictionary."""
return {
"engine_id": self.engine_id,
"region": self.region,
}
def get_client_options(self) -> Optional[ClientOptions]:
"""Generates API client options based on the application's region.
Returns:
ClientOptions configured with the regional endpoint, or None if
the region is 'global'.
"""
return (
ClientOptions(
api_endpoint=f"{self.region}-discoveryengine.googleapis.com"
)
if self.region != "global"
else None
)
def get_serving_config(self) -> str:
"""Constructs the full serving config path for the Discovery Engine API.
Returns:
The formatted serving config string.
"""
serving_config = f"projects/{PROJECT_ID}/locations/{self.region}"
serving_config += f"/collections/default_collection/engines/{self.engine_id}"
serving_config += "/servingConfigs/default_config"
return serving_config
class Engine(BaseModel):
"""Represents a discovered Discovery Engine instance."""
name: str
engine_id: str
region: str
@dataclass
class SearchResult:
"""Represents a single document result from a search query."""
document_id: str
title: str
snippet: str
link: Optional[str] = None
content: Optional[str] = None
@dataclass
class SearchResultsWithSummary:
"""Represents the complete results of a search, including a summary."""
results: List[SearchResult]
summary: Optional[str] = None
@@ -0,0 +1,14 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
@@ -0,0 +1,318 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Provides a repository class for interacting with Google BigQuery.
This module defines the `BigQueryRepository` class, which encapsulates
common BigQuery operations like running queries, fetching rows by ID,
inserting, updating, and deleting rows. It relies on the 'BIG_QUERY_DATASET'
environment variable to target the correct dataset.
"""
import datetime
from decimal import Decimal
from os import getenv
from typing import Dict, List, Any, Optional, Tuple
from google.cloud.bigquery import (
Client,
QueryJobConfig,
ScalarQueryParameter,
ArrayQueryParameter,
TableReference,
DatasetReference
)
from google.cloud.bigquery.table import RowIterator
from google.cloud.exceptions import GoogleCloudError
_BIG_QUERY_DATASET_ENV = getenv("BIG_QUERY_DATASET")
if not _BIG_QUERY_DATASET_ENV:
raise ValueError(
"The BIG_QUERY_DATASET environment variable is not set. "
"This is required for the BigQueryRepository to function."
)
BIG_QUERY_DATASET: str = _BIG_QUERY_DATASET_ENV
class BigQueryRepository:
"""
A repository class for simplifying interactions with Google BigQuery,
using parameterized queries to prevent SQL injection.
"""
def __init__(self):
"""Initializes the BigQuery client and project ID."""
self.client: Client = Client()
self.project_id: str = self.client.project
self.dataset_id: str = BIG_QUERY_DATASET
def _get_table_ref(self, table_id: str) -> TableReference:
"""Helper to get a TableReference object."""
dataset_ref = DatasetReference(self.project_id, self.dataset_id)
return TableReference(dataset_ref, table_id)
def _validate_column_name(self, column_name: str):
"""Basic validation for column names."""
if not column_name.replace("_", "").isalnum():
raise ValueError(f"Invalid column name: {column_name}")
def _create_scalar_query_parameter(self, name: str, value: Any) -> ScalarQueryParameter:
"""
Creates a ScalarQueryParameter by inferring the BigQuery type from the value.
"""
if isinstance(value, bool):
return ScalarQueryParameter(name, "BOOL", value)
if isinstance(value, int):
return ScalarQueryParameter(name, "INT64", value)
if isinstance(value, float):
return ScalarQueryParameter(name, "FLOAT64", value)
if isinstance(value, str):
return ScalarQueryParameter(name, "STRING", value)
if isinstance(value, datetime.datetime):
return ScalarQueryParameter(name, "TIMESTAMP", value)
if isinstance(value, datetime.date):
return ScalarQueryParameter(name, "DATE", value)
if isinstance(value, Decimal):
return ScalarQueryParameter(name, "NUMERIC", value)
if value is None:
# For None, the type of the column in BQ will ultimately determine how NULL is handled.
# Using STRING type for the parameter is a common safe default.
return ScalarQueryParameter(name, "STRING", None) # Default to STRING for None
# Fallback: attempt to convert other types to string.
try:
str_value = str(value)
return ScalarQueryParameter(name, "STRING", str_value)
except Exception as e:
raise TypeError(
f"Value for param '{name}' of type {type(value)} "
f"could not be converted to a supported BigQuery scalar type: {e}"
)
def run_query(self, query: str, job_config: Optional[QueryJobConfig] = None) -> RowIterator:
"""
Executes a BigQuery SQL query and returns the results.
Args:
query: The SQL query string to execute.
job_config: Optional QueryJobConfig for parameterized queries.
Returns:
A RowIterator object to iterate over the query results.
Raises:
google.cloud.exceptions.GoogleCloudError: If the query fails.
"""
try:
query_job = self.client.query(query, job_config=job_config)
return query_job.result()
except GoogleCloudError as e:
print(f"BigQuery Error: {e}")
print(f"Query: {query}")
if job_config and job_config.query_parameters:
print(f"Parameters: {job_config.query_parameters}")
raise
def get_row_by_id(self, table_id: str, id_column: str, id_value: Any, id_value_type: str = "STRING") -> RowIterator:
"""
Retrieves a single row from a table based on its ID using parameterized query.
Args:
table_id: The ID of the table (without dataset prefix).
id_column: The name of the column containing the ID.
id_value: The specific ID value to search for.
id_value_type: The BigQuery type of the ID value (e.g., "STRING", "INT64").
Returns:
A RowIterator containing the matching row(s).
"""
self._validate_column_name(id_column)
query = f"""
SELECT * FROM `{self.project_id}.{self.dataset_id}.{table_id}`
WHERE `{id_column}` = @id_value;
"""
job_config = QueryJobConfig(
query_parameters=[
ScalarQueryParameter("id_value", id_value_type, id_value)
]
)
return self.run_query(query, job_config=job_config)
def insert_rows_json(self, table_id: str, rows: List[Dict[str, Any]]) -> List[Dict]:
"""
Inserts multiple rows into the specified table using JSON.
This is the recommended method for insertions due to its safety and efficiency.
Args:
table_id: The ID of the table.
rows: A list of dictionaries, where each dictionary represents a row
(column names as keys, values as row data).
Returns:
A list of error dictionaries if any rows failed to insert,
otherwise an empty list.
"""
table_ref = self._get_table_ref(table_id)
errors_from_client = self.client.insert_rows_json(table_ref, rows)
errors_list: List[Dict[str, Any]] = list(errors_from_client) # type: ignore
if errors_list:
print(f"Errors inserting rows into {table_id}: {errors_list}")
return errors_list
def insert_row(self, table_id: str, column_names: List[str], values: Tuple[Any, ...]) -> None:
"""
Inserts a new row into the specified table using a parameterized query.
Prefer `insert_rows_json` for multiple rows or complex data.
Args:
table_id: The ID of the table.
column_names: A list of column names for the insert.
values: A tuple of values corresponding to the column_names.
Raises:
ValueError: If column names are invalid or counts don't match.
TypeError: If a value in `values` cannot be mapped to a BigQuery scalar type.
"""
if not column_names or not values:
raise ValueError("Column names and values cannot be empty.")
if len(column_names) != len(values):
raise ValueError("Number of column names must match number of values.")
for col_name in column_names:
self._validate_column_name(col_name)
cols_str = ", ".join(f"`{col}`" for col in column_names)
placeholders = ", ".join([f"@param{i}" for i in range(len(values))])
query_params: List[ScalarQueryParameter] = []
for i, current_value in enumerate(values):
param_name = f"param{i}"
query_params.append(self._create_scalar_query_parameter(param_name, current_value))
query = f"""
INSERT INTO `{self.project_id}.{self.dataset_id}.{table_id}` ({cols_str})
VALUES ({placeholders});
"""
job_config = QueryJobConfig(query_parameters=query_params)
self.run_query(query, job_config=job_config)
def delete_multiple_rows_by_id(
self, table_id: str, id_column: str, ids: List[Any], id_value_type: str = "STRING"
) -> None:
"""
Deletes multiple rows from a table based on a list of IDs using parameterized query.
Args:
table_id: The ID of the table.
id_column: The name of the column containing the IDs.
ids: A list of ID values to delete.
id_value_type: The BigQuery type of the ID values.
"""
if not ids:
return # Nothing to delete
self._validate_column_name(id_column)
query = f"""
DELETE FROM `{self.project_id}.{self.dataset_id}.{table_id}`
WHERE `{id_column}` IN UNNEST(@ids);
"""
job_config = QueryJobConfig(
query_parameters=[
ArrayQueryParameter("ids", id_value_type, ids)
]
)
self.run_query(query, job_config=job_config)
def update_row_by_id(
self,
table_id: str,
id_column: str,
id_value: Any,
column_values: Dict[str, Any],
id_value_type: str = "STRING",
) -> None:
"""
Updates specific columns of a row identified by its ID using parameterized query.
Args:
table_id: The ID of the table.
id_column: The name of the column containing the ID.
id_value: The specific ID value of the row to update.
column_values: A dictionary where keys are column names and
values are the new values.
id_value_type: The BigQuery type of the ID value.
"""
if not column_values:
return
self._validate_column_name(id_column)
set_clauses = []
query_params_list: List[ScalarQueryParameter] = [
ScalarQueryParameter("id_value", id_value_type, id_value)
]
param_idx = 0
for col, current_value in column_values.items():
self._validate_column_name(col)
update_param_name = f"update_val_{param_idx}"
set_clauses.append(f"`{col}` = @{update_param_name}")
query_params_list.append(self._create_scalar_query_parameter(update_param_name, current_value))
param_idx += 1
if not set_clauses: # Should not happen if column_values is not empty
return
sets_str = ", ".join(set_clauses)
query = f"""
UPDATE `{self.project_id}.{self.dataset_id}.{table_id}`
SET {sets_str}
WHERE `{id_column}` = @id_value;
"""
job_config = QueryJobConfig(query_parameters=query_params_list)
self.run_query(query, job_config=job_config)
def get_all_rows(self, table_id: str) -> RowIterator:
"""
Retrieves all rows from the specified table.
Args:
table_id: The ID of the table.
Returns:
A RowIterator containing all rows in the table.
"""
query = f"SELECT * FROM `{self.project_id}.{self.dataset_id}.{table_id}`"
return self.run_query(query)
def delete_row_by_id(self, table_id: str, id_column: str, id_value: Any, id_value_type: str = "STRING") -> None:
"""
Deletes a single row from a table based on its ID using parameterized query.
Args:
table_id: The ID of the table.
id_column: The name of the column containing the ID.
id_value: The specific ID value of the row to delete.
id_value_type: The BigQuery type of the ID value.
"""
self._validate_column_name(id_column)
query = f"""
DELETE FROM `{self.project_id}.{self.dataset_id}.{table_id}`
WHERE `{id_column}` = @id_value;
"""
job_config = QueryJobConfig(
query_parameters=[
ScalarQueryParameter("id_value", id_value_type, id_value)
]
)
self.run_query(query, job_config=job_config)
@@ -0,0 +1,14 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
@@ -0,0 +1,77 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Service for interacting with Google Cloud Discovery Engine Engines."""
from typing import List
from google.cloud.discoveryengine_v1 import (
EngineServiceClient,
ListEnginesRequest,
)
from google.api_core.client_options import ClientOptions
from src.model.search import PROJECT_ID, Engine
LOCATIONS = [
"global",
"us",
]
class EngineService:
"""Provides methods to list Discovery Engine Engines."""
def get_all(self) -> List[Engine]:
"""
Retrieves all available Discovery Engines for the configured project
across specified locations.
Fetches engines from predefined locations ('global', 'us') within the
default collection.
Returns:
A list of Engine objects, each containing details about a
discovered engine. Returns an empty list if no engines are found
or if an error occurs during API calls.
Raises:
Prints error logs if API calls fail for a specific location.
"""
engines = []
for location in LOCATIONS:
client_options = (
ClientOptions(
api_endpoint=f"{location}-discoveryengine.googleapis.com"
)
if location != "global"
else None
)
# Create a client
client = EngineServiceClient(client_options=client_options)
list_engines = client.list_engines(
ListEnginesRequest(
parent=f"projects/{PROJECT_ID}/locations/{location}"
"/collections/default_collection"
)
)
for engine in list_engines.engines:
engines.append(
Engine(
name=engine.display_name,
engine_id=engine.name.split("/")[-1],
region=location,
)
)
return engines
@@ -0,0 +1,144 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Service for performing searches using Google Cloud Discovery Engine."""
from google.cloud.discoveryengine_v1 import SearchRequest, SearchServiceClient
from src.model.search import (
SearchApplication,
SearchResult,
SearchResultsWithSummary,
)
CONTENT_SEARCH_SPEC = SearchRequest.ContentSearchSpec(
snippet_spec=SearchRequest.ContentSearchSpec.SnippetSpec(
return_snippet=True
),
extractive_content_spec=SearchRequest.ContentSearchSpec.ExtractiveContentSpec(
max_extractive_answer_count=1
),
summary_spec=SearchRequest.ContentSearchSpec.SummarySpec(
summary_result_count=5,
include_citations=True,
ignore_adversarial_query=True,
ignore_non_summary_seeking_query=True,
model_spec=SearchRequest.ContentSearchSpec.SummarySpec.ModelSpec(
version="stable",
),
),
)
QUERY_EXPANSION_SPEC = SearchRequest.QueryExpansionSpec(
condition=SearchRequest.QueryExpansionSpec.Condition.AUTO,
)
SPELL_CORRECTION_SPEC = SearchRequest.SpellCorrectionSpec(
mode=SearchRequest.SpellCorrectionSpec.Mode.AUTO
)
class SearchService:
"""
Handles search operations using a configured
Discovery Engine Search Application.
Initializes with a specific SearchApplication configuration and provides
methods to perform searches against the corresponding engine.
"""
def __init__(self, search_application: SearchApplication):
"""
Initializes the SearchService.
Args:
search_application: The configuration object defining the target
search engine, location, and serving config.
"""
self.search_client = SearchServiceClient(
client_options=search_application.get_client_options()
)
self.serving_config = search_application.get_serving_config()
def search(self, term: str) -> SearchResultsWithSummary:
"""
Performs a search against the configured Discovery Engine.
Constructs a search request with predefined content specifications,
query expansion, and spell correction settings. Parses the response
to extract results, snippets, links, and a summary.
Args:
term: The search query string.
Returns:
A SearchResultsWithSummary object containing the search summary
and a list of SearchResult objects. Returns an empty list of
results and a default summary message if the search fails or
yields no results.
Raises:
Logs errors if the API call fails.
"""
request = SearchRequest(
serving_config=self.serving_config,
query=term,
page_size=10,
content_search_spec=CONTENT_SEARCH_SPEC,
query_expansion_spec=QUERY_EXPANSION_SPEC,
spell_correction_spec=SPELL_CORRECTION_SPEC,
)
data = self.search_client.search(request)
results = []
summary_text = (
data.summary.summary_text
if data.summary and data.summary.summary_text
else "No summary available"
)
# Process results
for r in data.results:
document = r.document
derived_data = document.derived_struct_data
gcs_link = derived_data.get("link").replace(
"gs://", "https://storage.cloud.google.com/"
)
# Extract snippet safely
snippets = derived_data.get("snippets", [])
snippet_text = (
snippets[0].get("snippet", "No snippet available")
if snippets
else "No snippet available"
)
extractive_answers = derived_data.get("extractive_answers", [])
content_text = (
extractive_answers[0].get("content", "No content available")
if extractive_answers
else "No content available"
)
# Map to SearchResult
mapped_result = SearchResult(
document_id=document.id,
title=derived_data.get("title", "Untitled"),
snippet=snippet_text,
link=gcs_link,
content=content_text,
)
results.append(mapped_result)
response_result = SearchResultsWithSummary(
summary=summary_text, results=results
)
return response_result
@@ -0,0 +1,119 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Service for managing the Search Application configuration
stored in BigQuery."""
from src.model.http_status import BadRequest
from src.model.search import SearchApplication
from src.repository.big_query import BigQueryRepository
SEARCH_APPLICATION_TABLE = "search_applications"
SEARCH_APPLICATION_TABLE_ID_COLUMN = "engine_id"
class SearchApplicationService:
"""Handles business logic for Search Application configuration."""
def __init__(self):
"""Initializes the service with a BigQuery repository."""
self.repository = BigQueryRepository()
def get(self):
"""
Retrieves the currently configured Search Application.
Assumes that there is at most one Search Application configuration
stored in the BigQuery table. If multiple rows exist, it returns
the last one processed.
Returns:
A SearchApplication object if found, otherwise None.
"""
search_application = None
results = self.repository.get_all_rows(SEARCH_APPLICATION_TABLE)
# Convert RowIterator to list to check if it's empty or get the last item
rows = list(results)
if rows:
# Assuming we take the last row if multiple exist, or the only one
search_application = SearchApplication.from_row(rows[-1])
return search_application
def create(
self, search_application: SearchApplication
) -> SearchApplication:
"""
Creates a new Search Application configuration in BigQuery.
Ensures that no configuration already exists before creating a new one.
Args:
search_application: The SearchApplication object to create.
Returns:
The created SearchApplication object.
Raises:
BadRequest: If a Search Application configuration already exists
for the project.
"""
if self.get():
raise BadRequest(
detail="Search Application for this project already exists"
)
schema_fields = SearchApplication.__schema__()
column_names = [field.name for field in schema_fields]
# Prepare values as a tuple, matching the order of schema_fields
# Ensure the order here matches the order in SearchApplication.__schema__
values_tuple = (search_application.engine_id, search_application.region)
self.repository.insert_row(
SEARCH_APPLICATION_TABLE,
column_names,
values_tuple,
)
return search_application
def update(self, engine_id: str, search_application: SearchApplication):
"""
Updates an existing Search Application configuration in BigQuery.
Identifies the row to update using the provided engine_id.
Args:
engine_id: The engine_id of the Search Application configuration
to update.
search_application: A SearchApplication object containing the
updated details (engine_id and region).
Returns:
The updated SearchApplication object.
Raises:
# Note: The underlying repository might raise exceptions on failure,
# which are not explicitly handled here.
# Consider adding error handling.
"""
update_dict = {
"engine_id": f'"{search_application.engine_id}"',
"region": f'"{search_application.region}"',
}
self.repository.update_row_by_id(
SEARCH_APPLICATION_TABLE,
SEARCH_APPLICATION_TABLE_ID_COLUMN,
engine_id,
update_dict,
)
@@ -0,0 +1,65 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
services:
backend:
build:
secrets:
- gcp_credentials
context: ./backend
dockerfile: Dockerfile.local
args:
IS_FIRST_DEPLOYMENT: "True"
GCLOUD_PROJECT: my-project-id
ENVIRONMENT: development
FRONTEND_URL: http://localhost:4200
BIG_QUERY_DATASET: bigquery_quickbot_document_search
# Replace to override default variables if needed
VERTEX_AI_LOCATION: global
VERTEX_AI_DATASTORE_ID: quickbot_alphabet_pdfs_ds
VERTEX_AI_ENGINE_ID: quickbot_alphabet_search_engine
container_name: quickbot-document-search-backend
ports:
- "8080:8080"
volumes:
- ./backend:/app
# Mount the gcloud ADC directory. Replace with the correct path for your OS if different. :ro - read-only for better security
- ~/.config/gcloud/:/root/.config/gcloud:ro # Linux/macOS example:
# - %APPDATA%/gcloud:/root/.config/gcloud:ro # Windows example:
environment:
GOOGLE_APPLICATION_CREDENTIALS: "/root/.config/gcloud/application_default_credentials.json"
restart: unless-stopped
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
container_name: quickbot-document-search-frontend
ports:
- "4200:8080"
volumes:
- ./frontend:/app
# Use an anonymous volume for node_modules to prevent host node_modules
# from interfering and potentially speed up builds/rebuilds
- /app/node_modules
# To change the env variables in the frontend, change the environment.ts file
restart: unless-stopped
secrets:
gcp_credentials: # This ID must match the one used in services.backend.build.secrets
# Path to your Google Application Default Credentials JSON file on the HOST machine.
# Replace with the correct path for your OS if different.
file: ~/.config/gcloud/application_default_credentials.json
# Windows example (you might need to adjust the target path inside the container if it's a Linux container):
# - %APPDATA%/gcloud/application_default_credentials.json
@@ -0,0 +1,4 @@
.angular
.vscode
dist
node_modules
@@ -0,0 +1,8 @@
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
insert_final_newline = true
@@ -0,0 +1,3 @@
{
"extends": "./node_modules/gts/"
}
@@ -0,0 +1,42 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# Compiled output
/dist
/tmp
/out-tsc
/bazel-out
# Node
/node_modules
npm-debug.log
yarn-error.log
# IDEs and editors
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# Miscellaneous
/.angular/cache
.sass-cache/
/connect.lock
/coverage
/libpeerconnection.log
testem.log
/typings
# System files
.DS_Store
Thumbs.db
@@ -0,0 +1,33 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
module.exports = {
...require('gts/.prettierrc.json'),
"overrides": [
{
"files": "*.html",
"options": {
"parser": "angular"
}
},
{
"files": "*.scss",
"options": {
"parser": "scss"
}
}
]
}
@@ -0,0 +1,27 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
FROM node:18.17.1-alpine AS builder
WORKDIR /app
COPY . /app/
RUN npm ci
RUN npm run build:prod
FROM nginx:alpine
COPY --from=builder /app/dist/quick-bot-app-frontend /usr/share/nginx/html
COPY --from=builder /app/nginx.conf /etc/nginx/conf.d/
EXPOSE 8080
@@ -0,0 +1,88 @@
# QuickBotAppFrontend
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 15.1.3.
## Development server
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files.
## Code scaffolding
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
## Build
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
## Running unit tests
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Running end-to-end tests
Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.
## Code Styling & Commit Guidelines
To maintain code quality and consistency:
* **TypeScript (Frontend):** We follow [Angular Coding Style Guide](https://angular.dev/style-guide) by leveraging the use of [Google's TypeScript Style Guide](https://github.com/google/gts) using `gts`. This includes a formatter, linter, and automatic code fixer.
* **Python (Backend):** We adhere to the [Google Python Style Guide](https://google.github.io/styleguide/pyguide.html), using tools like `pylint` and `black` for linting and formatting.
* **Commit Messages:** We suggest following [Angular's Commit Message Guidelines](https://github.com/angular/angular/blob/main/contributing-docs/commit-message-guidelines.md) to create clear and descriptive commit messages.
#### Frontend (TypeScript with `gts`)
1. **Initialize `gts` (if not already done in the project):**
Navigate to the `frontend/` directory and run:
```bash
npx gts init
```
This will set up `gts` and create necessary configuration files (like `tsconfig.json`). Ensure your `tsconfig.json` (or a related gts config file like `.gtsrc`) includes an extension for `gts` defaults, typically:
```json
{
"extends": "./node_modules/gts/tsconfig-google.json",
// ... other configurations
}
```
2. **Check for linting issues:**
```bash
npm run lint
```
(This assumes a `lint` script is defined in `package.json`, e.g., `"lint": "gts lint"`)
3. **Fix linting issues automatically (where possible):**
```bash
npm run fix
```
(This assumes a `fix` script is defined in `package.json`, e.g., `"fix": "gts fix"`)
#### Backend (Python with `pylint` and `black`)
1. **Ensure Dependencies are Installed:**
Add `pylint` and `black` to your `backend/requirements.txt` file:
```
pylint
black
```
Then install them within your virtual environment:
```bash
pip install pylint black
# or pip install -r requirements.txt
```
2. **Configure `pylint`:**
It's recommended to have a `.pylintrc` file in your `backend/` directory to configure `pylint` rules. You might need to copy a standard one or generate one (`pylint --generate-rcfile > .pylintrc`).
3. **Check for linting issues with `pylint`:**
Navigate to the `backend/` directory and run:
```bash
pylint .
```
(Or specify modules/packages: `pylint your_module_name`)
4. **Format code with `black`:**
To automatically format all Python files in the current directory and subdirectories:
```bash
python -m black . --line-length=80
```
@@ -0,0 +1,123 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"quick-bot-app-frontend": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/quick-bot-app-frontend",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": [
"zone.js"
],
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"@angular/material/prebuilt-themes/indigo-pink.css",
"src/styles.scss",
"node_modules/prismjs/themes/prism-okaidia.css"
],
"scripts": [
"node_modules/marked/marked.min.js",
"node_modules/prismjs/prism.js",
"node_modules/prismjs/components/prism-csharp.min.js",
"node_modules/prismjs/components/prism-css.min.js",
"node_modules/clipboard/dist/clipboard.min.js"
]
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "4mb",
"maximumError": "10mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "20kb",
"maximumError": "50kb"
}
],
"outputHashing": "all"
},
"development": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"browserTarget": "quick-bot-app-frontend:build:production"
},
"development": {
"browserTarget": "quick-bot-app-frontend:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "quick-bot-app-frontend:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"polyfills": [
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json",
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"@angular/material/prebuilt-themes/indigo-pink.css",
"src/styles.scss",
"node_modules/prismjs/themes/prism-okaidia.css"
],
"scripts": [
"node_modules/marked/marked.min.js",
"node_modules/prismjs/prism.js",
"node_modules/prismjs/components/prism-csharp.min.js",
"node_modules/prismjs/components/prism-css.min.js",
"node_modules/clipboard/dist/clipboard.min.js"
]
}
}
}
}
},
"cli": {
"analytics": false
}
}
@@ -0,0 +1,11 @@
server {
listen 8080;
root /usr/share/nginx/html;
index index.html index.htm;
include /etc/nginx/mime.types;
location / {
try_files $uri $uri/ /index.html;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,71 @@
{
"name": "quick-bot-app-frontend",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build:prod": "ng build --configuration production --aot --output-hashing=all",
"build:dev": "ng build --configuration development",
"build:staging": "ng build --configuration staging",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test",
"postinstall": "ngcc",
"post-build": "node ./build/post-build.js",
"lint": "gts lint",
"clean": "gts clean",
"compile": "tsc",
"fix": "gts fix",
"prepare": "npm run compile",
"pretest": "npm run compile",
"posttest": "npm run lint"
},
"private": true,
"dependencies": {
"@angular/animations": "^15.1.0",
"@angular/cdk": "^15.2.9",
"@angular/common": "^15.1.0",
"@angular/compiler": "^15.1.0",
"@angular/core": "^15.1.0",
"@angular/fire": "7.5",
"@angular/flex-layout": "^15.0.0-beta.42",
"@angular/forms": "^15.1.0",
"@angular/material": "^15.2.9",
"@angular/platform-browser": "^15.1.0",
"@angular/platform-browser-dynamic": "^15.1.0",
"@angular/router": "^15.1.0",
"@fortawesome/fontawesome-free": "^6.6.0",
"@ionic/angular": "^7.5.4",
"@ng-idle/core": "^14.0.0",
"clipboard": "^2.0.11",
"ionic-emoji-rating": "^1.0.15",
"marked": "^4.3.0",
"moment": "^2.30.1",
"ng2-pdf-viewer": "^7.0.2",
"ngx-markdown": "^15.1.2",
"prismjs": "^1.29.0",
"rxfire": "6.0.3",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"uuid": "^9.0.1",
"zone.js": "~0.12.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "^15.1.3",
"@angular/cli": "~15.1.3",
"@angular/compiler-cli": "^15.1.0",
"@types/jasmine": "~4.3.0",
"@types/marked": "^4.3.0",
"@types/uuid": "^9.0.4",
"firebase-tools": "^11.30.0",
"gts": "^5.3.1",
"jasmine-core": "~4.5.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.0.0",
"typescript": "~4.9.4",
"@types/node": "20.12.7"
}
}
@@ -0,0 +1,41 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
import {MainComponent} from './components/main/main.component';
import {LoginComponent} from './components/login/login.component';
import {AuthGuard} from './services/login/auth.guard';
import {SearchResultsComponent} from './components/main/search-results/search-results.component';
import {ManageSearchApplicationComponent} from './components/search-application/manage-search-application/manage-search-application.component';
const routes: Routes = [
{path: '', component: MainComponent, canActivate: [AuthGuard]},
{path: 'login', component: LoginComponent},
{path: 'search', component: SearchResultsComponent, canActivate: [AuthGuard]},
// TODO: Add new AddNewAgentComponent
{
path: 'manage-config',
component: ManageSearchApplicationComponent,
canActivate: [AuthGuard],
},
];
@NgModule({
imports: [RouterModule.forRoot(routes, {onSameUrlNavigation: 'reload'})],
exports: [RouterModule],
})
export class AppRoutingModule {}
@@ -0,0 +1,18 @@
<!--
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<router-outlet></router-outlet>
<app-footer *ngIf="showHeader"></app-footer>
@@ -0,0 +1,98 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
::ng-deep .tour-step {
background: white;
}
::ng-deep .tour-top{
margin: -1%;
}
::ng-deep .tour-left{
margin-left: -1%;
}
::ng-deep .tour-top-right{
margin-top: -1%;
}
::ng-deep .tour-bottom-right{
margin-top: 1%;
}
::ng-deep .tour-bottom-left{
margin-top: 1%;
}
::ng-deep .tour-bottom{
margin: 1%;
}
::ng-deep ngx-guided-tour .tour-content{
font-size: 17px !important;
padding-bottom: 20px !important;
}
::ng-deep ngx-guided-tour .tour-title{
font-size: 22px !important;
padding-bottom: 0 !important;
}
::ng-deep ngx-guided-tour .tour-step .tour-buttons .skip-button{
font-weight: 500 !important;
font-size: 17px !important;
line-height: 1.7 !important;
}
::ng-deep ngx-guided-tour .tour-step{
border-radius: 20px !important;
}
::ng-deep ngx-guided-tour .tour-step .tour-buttons .back-button{
font-weight: 500 !important;
font-size: 17px !important;
line-height: 1.7 !important;
}
::ng-deep ngx-guided-tour .tour-step .tour-buttons .next-button{
background: #4285F4;
padding-left: 16px !important;
padding-right: 16px !important;
padding-top: 6px !important;
padding-bottom: 6px !important;
font-size: 16px !important;
color: white;
font-weight: 500;
border-radius: 15px !important;
}
::ng-deep ngx-guided-tour .tour-step.tour-top .tour-block{
margin-bottom: 0 !important;
}
::ng-deep ngx-guided-tour .tour-step.tour-bottom-left .tour-block{
margin-top: 0 !important;
}
::ng-deep pre{
background: #485f84 !important;
}
div {
font-family: "Google Sans", sans-serif !important;
}
@@ -0,0 +1,49 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {TestBed} from '@angular/core/testing';
import {RouterTestingModule} from '@angular/router/testing';
import {AppComponent} from './app.component';
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [RouterTestingModule],
declarations: [AppComponent],
}).compileComponents();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it("should have as title 'pac-assist-ui'", () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app.title).toEqual('pac-assist-ui');
});
it('should render title', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('.content span')?.textContent).toContain(
'pac-assist-ui app is running!'
);
});
});
@@ -0,0 +1,55 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Component} from '@angular/core';
import {Router, NavigationEnd, Event as NavigationEvent} from '@angular/router';
import {UserService} from './services/user/user.service';
import {AuthService} from './services/login/auth.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent {
title = 'pac-assist-ui';
showHeader = true;
userInfo: any;
constructor(
private router: Router,
private userService: UserService,
public authService: AuthService
) {
this.router.events.subscribe((event: NavigationEvent) => {
if (event instanceof NavigationEnd) {
if (
event.url === '/login' ||
event.url === '/login/e2e' ||
(event.url.includes('login') && event.url.includes('email')) ||
(event.url.includes('login') && event.url.includes('tos')) ||
event.url.includes('reset-password') ||
event.url.includes('support-ticket')
) {
this.showHeader = false;
} else {
this.userInfo = this.userService.getUserDetails();
this.showHeader = true;
}
}
});
}
}
@@ -0,0 +1,167 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {AppRoutingModule} from './app-routing.module';
import {AppComponent} from './app.component';
import {HeaderComponent} from './components/header/header.component';
import {FooterComponent} from './components/footer/footer.component';
import {MainComponent} from './components/main/main.component';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {MatIconModule} from '@angular/material/icon';
import {MatAutocompleteModule} from '@angular/material/autocomplete';
import {MatButtonModule} from '@angular/material/button';
import {MatToolbarModule} from '@angular/material/toolbar';
import {HttpClientModule} from '@angular/common/http';
import {MatInputModule} from '@angular/material/input';
import {NgFor, PathLocationStrategy} from '@angular/common';
import {MatSelectModule} from '@angular/material/select';
import {MatFormFieldModule} from '@angular/material/form-field';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {MatGridListModule} from '@angular/material/grid-list';
import {MatCardModule} from '@angular/material/card';
import {MatProgressSpinnerModule} from '@angular/material/progress-spinner';
import {MatTooltipModule} from '@angular/material/tooltip';
import {LoginComponent} from './components/login/login.component';
import {initializeApp, provideFirebaseApp} from '@angular/fire/app';
import {
provideAnalytics,
getAnalytics,
ScreenTrackingService,
UserTrackingService,
} from '@angular/fire/analytics';
import {environment} from '../environments/environment';
import {provideAuth, getAuth} from '@angular/fire/auth';
import {MatMenuModule} from '@angular/material/menu';
import {MatDividerModule} from '@angular/material/divider';
import {MatChipsModule} from '@angular/material/chips';
import {LocationStrategy} from '@angular/common';
import {MarkdownModule} from 'ngx-markdown';
import {MatDialogModule} from '@angular/material/dialog';
import {CdkAccordionModule} from '@angular/cdk/accordion';
import {MatExpansionModule} from '@angular/material/expansion';
import {IonicRatingModule} from 'ionic-emoji-rating';
import {MatListModule} from '@angular/material/list';
import {MatTabsModule} from '@angular/material/tabs';
import {MatTableModule} from '@angular/material/table';
import {MatCheckboxModule} from '@angular/material/checkbox';
import {MatSlideToggleModule} from '@angular/material/slide-toggle';
import {MatButtonToggleModule} from '@angular/material/button-toggle';
import {MatSnackBarModule} from '@angular/material/snack-bar';
import {MatSortModule} from '@angular/material/sort';
import {MatPaginatorModule} from '@angular/material/paginator';
import {MatSidenavModule} from '@angular/material/sidenav';
import {NgIdleModule} from '@ng-idle/core';
import {ClipboardModule} from '@angular/cdk/clipboard';
import {MatProgressBarModule} from '@angular/material/progress-bar';
import {ManageSearchApplicationComponent} from './components/search-application/manage-search-application/manage-search-application.component';
import {SearchApplicationFormComponent} from './components/search-application/search-application-form/search-application-form.component';
import {PdfViewerModule} from 'ng2-pdf-viewer';
import 'prismjs';
import 'prismjs/components/prism-typescript.min.js';
import 'prismjs/plugins/line-numbers/prism-line-numbers.js';
import 'prismjs/plugins/line-highlight/prism-line-highlight.js';
import {FlexLayoutModule} from '@angular/flex-layout';
import {MatSliderModule} from '@angular/material/slider';
import {MatStepperModule} from '@angular/material/stepper';
import {ChatInputComponent} from './components/main/chat-input/chat-input.component';
import {SearchResultsComponent} from './components/main/search-results/search-results.component';
import {ToastMessageComponent} from './components/toast-message/toast-message.component';
import {TruncatePipe} from './pipes/truncate.pipe';
@NgModule({
declarations: [
AppComponent,
HeaderComponent,
FooterComponent,
MainComponent,
LoginComponent,
ChatInputComponent,
SearchResultsComponent,
ToastMessageComponent,
SearchApplicationFormComponent,
ManageSearchApplicationComponent,
TruncatePipe,
],
imports: [
BrowserModule,
AppRoutingModule,
BrowserAnimationsModule,
MatSnackBarModule,
MatPaginatorModule,
MatSortModule,
MatSliderModule,
MatToolbarModule,
MatButtonModule,
MatIconModule,
HttpClientModule,
FormsModule,
MatFormFieldModule,
MatSelectModule,
NgFor,
MatInputModule,
MatGridListModule,
MatCardModule,
MatTooltipModule,
MatProgressSpinnerModule,
MatMenuModule,
MatDividerModule,
MatChipsModule,
ReactiveFormsModule,
MatMenuModule,
MatDialogModule,
CdkAccordionModule,
IonicRatingModule,
MarkdownModule.forRoot(),
MatExpansionModule,
MatListModule,
MatTabsModule,
MatCheckboxModule,
MatTableModule,
MatSlideToggleModule,
MatButtonToggleModule,
MatSidenavModule,
MatAutocompleteModule,
environment.requiredLogin === 'True'
? [
provideFirebaseApp(() => initializeApp(environment.firebase)),
provideAuth(() => getAuth()),
]
: [],
environment.requiredLogin === 'True'
? [provideAnalytics(() => getAnalytics())]
: [],
FlexLayoutModule,
NgIdleModule.forRoot(),
ClipboardModule,
MatStepperModule,
MatProgressBarModule,
PdfViewerModule,
],
providers: [
{provide: LocationStrategy, useClass: PathLocationStrategy},
environment.requiredLogin === 'True'
? [
ScreenTrackingService, // Automatically track screen views
UserTrackingService, // Automatically track user interactions
]
: [],
],
bootstrap: [AppComponent],
})
export class AppModule {}
@@ -0,0 +1,23 @@
<!--
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<p>
<mat-toolbar class="container footer" color="primary">
<div class="links-weight">
<span>Powered by <span class="genAI">Vertex AI</span></span>
</div>
</mat-toolbar>
</p>
@@ -0,0 +1,71 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.container {
padding-left: 4rem;
padding-right: 4rem;
}
.footer {
position: fixed;
bottom: 0;
}
.genAI {
background: linear-gradient(86.95deg, #34A853 -65.34%, #FBBC05 -12.75%, #EA4335 25.15%, #1A73E8 103.88%);
-webkit-background-clip: text;
-moz-background-clip: text;
background-clip: text;
color: transparent;
}
:host ::ng-deep .mat-toolbar.mat-primary {
background-color: transparent;
color: #504747;
box-shadow: none;
justify-content: center;
}
.line-spacer {
flex: 1 1 auto;
}
.links-weight {
margin-left: 10px;
margin-right: 10px;
font-size: 14px;
font-weight: 400;
cursor: pointer;
}
.links-bold {
margin-right: 10px;
color: #5f6368;
font-family: 'Google Sans', sans-serif !important;
.blue{
color: #4285f4;
}
.red{
color: #ea4335;
}
.yellow{
color: #fbbc04;
}
.green{
color: #34a853;
}
}
@@ -0,0 +1,38 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {FooterComponent} from './footer.component';
describe('FooterComponent', () => {
let component: FooterComponent;
let fixture: ComponentFixture<FooterComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [FooterComponent],
}).compileComponents();
fixture = TestBed.createComponent(FooterComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,24 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Component} from '@angular/core';
@Component({
selector: 'app-footer',
templateUrl: './footer.component.html',
styleUrls: ['./footer.component.scss'],
})
export class FooterComponent {}
@@ -0,0 +1,66 @@
<!--
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<mat-progress-bar *ngIf="showLoading" mode="indeterminate"></mat-progress-bar>
<nav class="navbar navbar-expand-lg navbar-light bg-light top-nav fixed-top">
<div class="parent-container">
<div class="left-heading" [matTooltip]="'Home'" [routerLink]="'/'">
<span class="logo-text gradient-text">{{headerTitle}}</span>
</div>
<app-chat-input (emitSearch)="this.searchTerm($event)" *ngIf="this.isSearchRoute()"></app-chat-input>
<div class="right-menu-items">
<div *ngIf="authService.isUserLoggedIn()" class="profile-container" [matMenuTriggerFor]="userAccountMenu">
<div class="circle" *ngIf="_UserService.getUserDetails().photoURL && requiredLogin === 'True'"><img referrerpolicy="no-referrer"
[src]="_UserService.getUserDetails().photoURL" class="user-icon" /> </div>
<div class="circle bypassLoginLogo" *ngIf="requiredLogin !== 'True'"><mat-icon>person</mat-icon></div>
</div>
</div>
</div>
<mat-menu #userAccountMenu class="profile-menu" [overlapTrigger]="false" yPosition="below">
<div class="card-inside-right">
<div class="name">
<span class="sub-title">
{{ requiredLogin === 'True' ? _UserService.getUserDetails().email : 'Guest'}}
</span>
<div *ngIf="requiredLogin === 'True' && _UserService.getUserDetails().photoURL ">
<img [src]="_UserService.getUserDetails().photoURL" class="user-icon container-circle" />
</div>
<div *ngIf="requiredLogin !== 'True'" class="user-icon container-circle bypassMenuContainer">
<mat-icon class="bypassMenuLogo">person</mat-icon>
</div>
</div>
<div class="action-item-container">
<br>
<div class="logout" (click)="goToManageConfig()">
<div class="logout-container">
<span class="material-symbols-rounded logo">settings</span>
<div class="text">
<span class="sub-title">Manage AgentBuilder Config</span>
</div>
</div>
</div>
<div class="logout logout-button" (click)="logout()" *ngIf="requiredLogin === 'True'">
<div class="logout-container">
<span class="material-symbols-rounded logo logout-button-text">exit_to_app</span>
<div class="text">
<span class="sub-title logout-button-text">Logout</span>
</div>
</div>
</div>
</div>
</div>
</mat-menu>
</nav>
@@ -0,0 +1,644 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.container {
margin: auto;
}
.logo-text {
font-family: Google Sans,Helvetica Neue,sans-serif;
font-style:normal;
font-size: 22px;
line-height: 128.7%;
letter-spacing: normal;
color: #2d2c2cbf;
}
.gradient-text {
font-family: Google Sans,Helvetica Neue,sans-serif;
font-style:normal;
font-size: 30px;
line-height: 128.7%;
letter-spacing: normal;
background: linear-gradient(89.9deg, rgba(66, 133, 244, 0.95) 21.17%, #A488F5 44.34%, rgba(234, 67, 53, 0.88) 81.26%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-fill-color: transparent;
}
.links-pointer {
cursor: pointer;
}
.logo-icon {
height: 35px;
width: 125px;
cursor: pointer;
}
// .profile-container {
// margin-left: 1%;
// font-family: 'Google Sans', sans-serif !important;
// .circle {
// display: flex;
// justify-content: center;
// width: 40px;
// height: 40px;
// border: 5px solid rgb(255, 255, 255);
// border-radius: 60%;
// overflow: hidden;
// }
// .circle img {
// width: 100%;
// height: 100%;
// object-fit: cover;
// }
// }
.card-inside-right {
display: flex;
flex-direction: column;
align-items: flex-start;
color: #3E4245;
font-size: 1rem;
font-weight: 400;
font-family: 'Google Sans', sans-serif !important;
border-radius: 5px;
letter-spacing: 0.4px;
.container-circle {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 50px;
padding-top: 4%;
padding-bottom: 4%;
}
.name {
display: flex;
align-items: center;
font-size: 1rem;
color: #3E4245;
font-weight: 500;
padding: 1.3em 0.4em;
flex-direction: column;
gap: 10px;
background: #DDE3EA !important;
width: -webkit-fill-available;;
.sub-title {
color: #3E4245;
}
margin: auto;
}
.company {
display: flex;
align-items: center;
padding: 0.3em 1em;
width: -webkit-fill-available;;
.logo {
padding-right: 2%;
color: #5F6368;
}
.text {
min-width: 100px;
.sub-title {
font-size: 0.85rem;
margin-bottom: 10px;
color: #3E4245;
}
}
margin-bottom: 10px;
}
.logout {
background: #DDE3EA;
border-radius: 20px;
font-size: 1rem;
font-weight: 500;
font-family: 'Google Sans', sans-serif !important;
margin-bottom: 1em;
cursor: pointer;
width: -webkit-fill-available;;
padding: 0.3em 1em;
.logout-container {
display: flex;
align-items: center;
.logo {
padding-right: 4%;
color: #5F6368;
}
.text {
.sub-title {
color: #3E4245;
font-size: 0.85rem;
font-family: 'Google Sans', sans-serif !important;
}
}
}
}
}
//------------css of card end-------------------//
.image {
width: 35px;
height: 35px;
}
.fixed-top {
z-index: 1000;
position: relative;
}
.top-nav {
height: 60px;
background-color: transparent;
// box-shadow: (0px 4px 16px rgba(195, 209, 226, 0.25));
.parent-container {
// width: 1600px;
display: flex;
margin: auto;
justify-content: space-between;
/* border: 1px solid black; */
height: 100%;
align-items: center;
padding: 0% 1.5%;
.left-heading {
display: flex;
align-items: center;
cursor: pointer;
}
.logo-icon {
height: 4.2em;
width: 4em;
cursor: pointer;
background-image: url('../../../assets/images/new-pdc-logo.svg');
background-size: 443px;
background-repeat: round;
border-radius: 28px;
margin-right:1em;
}
.user-icon {
height: 50px;
width: 50px;
cursor: pointer;
border-radius: 28px;
}
.right-menu-items {
display: flex;
align-items: center;
.profile-container {
margin-left: 1%;
font-family: 'Google Sans', sans-serif !important;
.circle {
display: flex;
justify-content: center;
width: 35px;
height: 35px;
border-radius: 60%;
overflow: hidden;
}
.circle img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
}
}
}
.icon-color {
color: #5F6368;
font-size: 24px;
}
::ng-deep .quick-link.mat-mdc-menu-panel.mat-mdc-menu-panel {
width: 420px !important;
display: flex;
flex-direction: column;
background: #F0F4F9;
.mat-mdc-menu-content {
padding: 0.5rem;
}
.ql-container {
width: 100%;
display: flex;
flex-direction: column;
.ql-child-container{
display: flex;
flex-direction: row;
text-wrap: nowrap ;
justify-content: space-between;
align-items: flex-end;
}
a, a:active{
text-decoration: none;
}
.align-link-content{
display: flex;
flex-direction: row;
align-items: baseline;
gap: 10px;
width: 100% !important;
box-shadow: 0px 4px 4px -6px #b9b8b8 !important;
}
.no-box-shadow{
box-shadow: none !important;
}
.icon-container {
display: flex;
alignment-baseline: after-edge;
mat-icon {
color: #161A1D !important;
}
}
}
.ql-header{
margin: 1rem;
margin-left: 1.7rem;
}
.avatar {
width: 40px;
height: 40px; //<--use the size you choose
border-radius: 100%;
text-align: center;
}
div.link-label {
color: #161A1D;
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 24px;
letter-spacing: 0.4px;
height: 50px;
margin: 0px 5px;
width: 57%;
}
.avatar span {
line-height: 40px;
font-size: 16px;
}
.link-button {
margin: 1rem;
height: 41px;
width: 100% !important;
}
}
.notification-banner-container {
z-index: 999;
position: relative;
width: inherit;
display: flex;
flex-direction: row;
height: 8%;
background: #F6F9FE;
align-content: center;
flex-wrap: wrap;
padding-left: 1%;
gap: 1%;
margin-bottom: 0.2%;
.amber {
color: #fbc645;
}
.red {
color: red;
}
.blue {
color: #4285F4;
}
}
.red-border {
border-left: 5px solid red;
}
.amber-border {
border-left: 5px solid #fbc645;
}
.blue-border {
border-left: 5px solid #4285F4;
}
.notification-text {
display: flex;
flex-direction: row;
align-items: center;
gap: 15px;
width: 70%;
}
.notification-buttons-container {
width: 26%;
justify-content: flex-end;
display: flex;
flex-direction: row;
gap: 15px;
button {
font-family: 'Google Sans', sans-serif !important;
font-style: normal;
font-weight: 500;
line-height: 23px;
font-size: 14px;
}
.dismiss-button {
letter-spacing: 0.4px;
color: #4285F4;
}
.learn-more-button {
color: #fff !important;
background: #4285F4 !important;
border-radius: 30px;
letter-spacing: unset;
box-shadow: none;
}
}
::ng-deep .profile-menu.mat-mdc-menu-panel.mat-mdc-menu-panel {
max-width: 320px !important;
width: 300px !important;
}
::ng-deep .profile-menu .mat-mdc-menu-content {
padding: 0% !important;
}
.action-item-container {
width: -webkit-fill-available;
padding-inline: 5%;
padding-top: 2%;
padding-bottom: 2%;
}
.logout-button {
background: white !important;
border: 1px solid #4285F4 !important;
}
.logout-button-text {
color: #4285F4 !important;
}
.app-version-chip {
font-size: 9px;
border-radius: -2px;
padding-inline: 8px;
line-height: 16px;
border-radius: 11px;
margin-left: 8px;
color: #474c55;
background: linear-gradient(92.1deg, #bdd1f3 19.16%, rgba(199, 183, 245, 0.9) 79%, rgba(235, 165, 158, 0.9) 135.24%), linear-gradient(272.04deg, rgba(158, 188, 237, 0.7) 14.93%, rgba(158, 188, 237, 0) 104.51%) !important;
}
.bypassLoginLogo {
background: aliceblue;
color: cornflowerblue;
align-items: center;
cursor: pointer;
}
.bypassMenuContainer {
display: flex;
justify-content: center;
background: aliceblue;
width: 42% !important;
height: 40% !important;
border-radius: 65px !important;
}
.bypassMenuLogo {
font-size: 6rem;
height: 6rem;
width: 6rem;
}
.chat-footer {
min-height: 60px;
display: flex;
border: 1px solid #DDE6F6;
box-shadow: 0px 4px 18px rgba(195, 209, 226, 0.25);
border-radius: 30px;
width: 66%;
margin-top: 25px;
// box-shadow: 5px 4px 24px rgba(66, 133, 244, 0.15);
.chat-textarea {
flex-grow: 1;
display: flex;
align-items: center;
padding-left: 2%;
input {
width: 100%;
height: 45px;
border: 0px;
border-radius: 4px;
resize: none;
outline: none;
padding: 14px;
font-family: 'Google Sans', sans-serif !important;
box-sizing: border-box;
-ms-overflow-style: none;
scrollbar-width: none;
font-size: 0.95rem;
line-height: 1.1rem;
color: #534646;
}
input::-webkit-scrollbar {
display: none;
}
}
.micIcon {
display: flex;
align-items: center;
color: #1F1F1F;
margin-top: 2px;
padding-right: 1%;
img {
max-width: 40%;
cursor: pointer;
}
}
}
.blue-dot {
/* Ellipse 1226 */
content: '';
width: 8px;
height: 8px;
left: 1503px;
top: 445px;
display: flow-root;
border-radius: 15px;
/* Blue */
background: #4285F4;
animation: blue-transform 2s infinite;
}
.green-dot {
content: '';
width:8px;
height: 8px;
left: 1522px;
top: 445px;
display: flow-root;
border-radius: 15px;
/* Green */
background: #0F9D58;
animation: green-transform 2s infinite;
}
.yellow-dot {
content: '';
width: 8px;
height: 8px;
left: 1541px;
top: 445px;
display: flow-root;
border-radius: 15px;
/* Yellow */
background: #F4B400;
animation: yellow-transform 2s infinite;
}
.recording-container {
display: flex;
flex-direction: row;
gap: 10px;
cursor: pointer;
align-items: baseline;
}
@keyframes blue-transform {
0% {
transform: translateY(-14px)
}
33% {
transform: translateY(0)
}
66% {
transform: translateY(0)
}
100% {
transform: translateY(-14px)
}
}
@keyframes green-transform {
0% {
transform: translateY(0)
}
33% {
transform: translateY(-14px)
}
66% {
transform: translateY(0)
}
100% {
transform: translateY(0)
}
}
@keyframes yellow-transform {
0% {
transform: translateY(0)
}
33% {
transform: translateY(0)
}
66% {
transform: translateY(-14px)
}
100% {
transform: translateY(0)
}
}
@@ -0,0 +1,38 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {HeaderComponent} from './header.component';
describe('HeaderComponent', () => {
let component: HeaderComponent;
let fixture: ComponentFixture<HeaderComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [HeaderComponent],
}).compileComponents();
fixture = TestBed.createComponent(HeaderComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,76 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Component, EventEmitter, Output} from '@angular/core';
import {DomSanitizer} from '@angular/platform-browser';
import {MatIconRegistry} from '@angular/material/icon';
import {UserService} from 'src/app/services/user/user.service';
import {AuthService} from '../../services/login/auth.service';
import {environment} from 'src/environments/environment';
import {Router} from '@angular/router';
const GOOGLE_CLOUD_ICON = `<svg width="694px" height="558px" viewBox="0 0 694 558" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="google-cloud-1" fill-rule="nonzero">
<path d="M440.545766,152.920305 L461.717489,152.920305 L522.033632,92.9270295 L525,67.4718307 C412.772422,-31.0513593 241.450145,-20.4353843 142.396729,91.1914478 C114.856041,122.200508 94.8766816,159.08162 84,199 C90.7179504,196.251996 98.1629517,195.8181 105.171723,197.72724 L225.774927,177.941608 C225.774927,177.941608 231.911237,167.846308 235.081179,168.482688 C288.737536,109.877878 379.037259,103.051256 440.981997,152.920305 L440.545766,152.920305 Z" id="Path" fill="#EA4335"></path>
<path d="M607.866504,200.682375 C594.001378,149.778725 565.57351,104.008948 526.012848,69 L441.426861,153.404341 C477.150633,182.525289 497.497778,226.409746 496.625757,272.440567 L496.625757,287.436115 C538.221135,287.436115 571.910193,321.081832 571.910193,362.558879 C571.910193,404.064932 538.192067,437.681644 496.625757,437.681644 L346.02782,437.681644 L331,452.880226 L331,542.998538 L346.02782,557.994086 L496.625757,557.994086 C582.955785,558.6612 659.548251,502.826713 685.185654,420.568736 C710.764921,338.281754 679.372184,248.946575 607.866504,200.682375 L607.866504,200.682375 Z" id="Path" fill="#4285F4"></path>
<path d="M194.75446,555.998385 L346,555.998385 L346,436.702654 L194.75446,436.702654 C183.982485,436.702654 173.327279,434.43008 163.518651,430 L142.266623,436.47252 L81.3130068,496.134769 L76,517.076966 C110.184236,542.506777 151.900097,556.170985 194.75446,555.998385 L194.75446,555.998385 Z" id="Path" fill="#34A853"></path>
<path d="M195.36137,165 C111.412398,165.496534 37.0600945,219.208571 10.2827643,298.683735 C-16.4945658,378.158899 10.1952567,465.881761 76.7302131,517 L164.383674,429.422857 C126.347031,412.257155 109.458061,367.550553 126.638723,329.547028 C143.819384,291.543502 188.564945,274.669238 226.601588,291.834941 C243.344712,299.412331 256.762546,312.818482 264.346539,329.547028 L352,241.969885 C314.692587,193.270582 256.733377,164.797082 195.36137,165 L195.36137,165 Z" id="Path" fill="#FBBC05"></path>
</g>
</g>
</svg>`;
@Component({
selector: 'app-header',
templateUrl: './header.component.html',
styleUrls: ['./header.component.scss'],
})
export class HeaderComponent {
headerTitle: string = environment.chatbotName;
requiredLogin: string = environment.requiredLogin;
showLoading = false;
@Output() emitSearch: EventEmitter<string> = new EventEmitter();
constructor(
iconRegistry: MatIconRegistry,
sanitizer: DomSanitizer,
public _UserService: UserService,
public authService: AuthService,
private router: Router
) {
iconRegistry.addSvgIconLiteral(
'google-cloud-icon',
sanitizer.bypassSecurityTrustHtml(GOOGLE_CLOUD_ICON)
);
}
isSearchRoute(): boolean {
return this.router.url.startsWith('/search');
}
searchTerm(term: string) {
this.emitSearch.emit(term);
}
goToManageConfig() {
this.router.navigateByUrl('/manage-config');
}
logout() {
this.authService.logout();
}
}
@@ -0,0 +1,48 @@
<!--
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="login-container">
<div class="left-container">
<div style="display: flex ;flex-direction:column;justify-content: center;align-items: center;">
<div class="parent-container"
style="margin: 0rem;width:90%;text-align: center;font-weight: 500; font-size: 1.5rem;">
<p class="login-app-text">
Quickly setup a <span class="linear-text">Vertex AI</span> powered Chat application
</p>
</div>
</div>
</div>
<div class="right-container">
<div class="login-card">
<p class="heading">{{chatbotName}}</p>
<div class="login-buttons-container">
<div>
<div class="register-text">
Single Sign on with Google Account.
</div>
<button (click)="loginWithGoogle()" class="login-button">
<span class="mat-button-wrapper"> Login in with Google </span>
</button>
</div>
</div>
<div class="spinner">
<mat-spinner [diameter]="50" *ngIf="this.loader"></mat-spinner>
</div>
</div>
</div>
</div>
@@ -0,0 +1,545 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.login-container {
display: flex;
min-height: 100vh;
max-height: 100%;
.spinner{
display: flex;
justify-content: center;
margin: 1em;
}
.linear-text {
background: linear-gradient(89.9deg, rgba(66, 133, 244, 0.95) 21.17%, #A488F5 44.34%, rgba(234, 67, 53, 0.88) 81.26%);
-webkit-background-clip: text;
-moz-background-clip: text;
background-clip: text;
color: transparent;
}
.heading {
font-family: 'Google Sans', sans-serif !important;
font-size: 32px;
font-style: normal;
font-weight: 500;
line-height: 150.7%;
color: rgba(45, 44, 44, 0.7490196078);
}
.left-container {
flex: 0 1 55%;
min-height: 100%;
max-width: 60%;
align-items: center;
background: #fafcfe;
padding: .5em;
overflow: auto;
display: flex;
justify-content: center;
.login-app-text-sub {
color: #161A1D;
font-size: 1rem;
font-style: normal;
font-weight: 400;
line-height: 150.7%;
}
.login-app-text {
// margin-bottom: 2rem;
line-height: 1.5em;
}
.expand-button {
--mdc-fab-container-color: #4285F41A;
--mdc-fab-icon-color: #000000;
border: 1px solid #4285F4;
--mat-mdc-fab-color: #4285F4;
height: 2.2rem;
font-size: 0.8rem;
font-weight: 400;
box-shadow: none;
}
.expand-less-button {
--mdc-fab-container-color: rgba(92, 95, 97, 0.10);
--mdc-fab-icon-color: #000000;
border: 1px solid rgba(92, 95, 97, 0.50);
--mat-mdc-fab-color: rgba(92, 95, 97, 0.50);
margin-bottom: 1em;
height: 2.2rem;
font-size: 0.8rem;
font-weight: 400;
box-shadow: none
}
}
.right-container {
flex: 0 1 45%;
min-height: 100%;
max-width: 80%;
align-items: flex-start;
display: flex;
justify-content: center;
overflow-y: auto;
scrollbar-width: thin;
.login-card {
flex-direction: column;
display: flex;
max-width: 100%;
padding: 3%;
margin: auto;
.logo-icon {
height: 145px;
width: 100%;
// margin-bottom: 2rem;
}
.login-button {
background: #4285F4;
box-sizing: border-box;
border: 1px solid #e0e0e0;
color: #fff;
position: relative;
user-select: none;
cursor: pointer;
outline: none;
-webkit-tap-highlight-color: transparent;
display: inline-block;
font-family: 'Google Sans', sans-serif !important;
font-size: 14px;
font-weight: 500;
white-space: nowrap;
text-decoration: none;
vertical-align: baseline;
text-align: center;
margin: 0;
min-width: 100px;
line-height: 36px;
padding: 0 16px;
border-radius: 4px;
overflow: visible;
transform: translate3d(0, 0, 0);
transition: background 400ms cubic-bezier(0.25, 0.8, 0.25, 1), box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1);
}
.register-text {
display: flex;
flex-direction: column;
align-items: flex-start;
margin: 1em 0em;
font-weight: 400;
color: #161A1D
}
.register-button {
background: white;
box-sizing: border-box;
border: 1px solid #4285F4;
color: #4285F4;
position: relative;
user-select: none;
cursor: pointer;
outline: none;
-webkit-tap-highlight-color: transparent;
display: inline-block;
font-family: 'Google Sans', sans-serif !important;
font-size: 14px;
font-weight: 500;
white-space: nowrap;
text-decoration: none;
vertical-align: baseline;
text-align: center;
margin: 0;
min-width: 64px;
line-height: 36px;
padding: 0 16px;
border-radius: 4px;
overflow: visible;
transform: translate3d(0, 0, 0);
transition: background 400ms cubic-bezier(0.25, 0.8, 0.25, 1), box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1);
}
}
}
.slide {
width: 100%;
min-height: 15rem;
border-radius: 10px;
background-size: cover;
background-position: center;
font-size: 1rem;
font-style: normal;
font-weight: 400;
line-height: 200.7%;
padding-left: 0px;
.feature-heading {
font-weight: 700;
}
}
.slider {
position: relative;
height: 100%;
}
.dotsContainer {
display: flex;
justify-content: center;
align-items: center;
margin-top: 2em;
}
.dot {
margin: 0 3px;
cursor: pointer;
font-size: 18px;
color: #D0D3D7;
}
.active {
color: #1A73E8;
background-color: #D0D3D7;
padding: 2px;
border-radius: 50%;
font-size: 12px;
}
.material-symbols-outlined {
font-variation-settings:
'FILL' 1,
'wght' 400,
'GRAD' 0,
'opsz' 18
}
h6 {
position: relative;
z-index: 1;
overflow: hidden;
text-align: center;
font-size: 1.2em;
font-style: normal;
font-weight: 500;
line-height: 200.7%;
font-family: 'Google Sans', sans-serif !important;
}
h6:before,
h6:after {
position: absolute;
top: 51%;
overflow: hidden;
width: 50%;
height: 1px;
content: '\a0';
background: linear-gradient(86.95deg, #34A853 -65.34%, #FBBC05 -12.75%, #EA4335 25.15%, #1A73E8 103.88%);
}
h6:before {
margin-left: -50%;
text-align: right;
}
}
.login-buttons-container {
padding-inline: 4%;
gap: 12px;
display: flex;
flex-direction: column;
background: #F7F9FF;
padding-top: 4%;
padding-bottom: 4%;
border-radius: 5px;
}
.back-button {
color: #5C5F61 !important;
font-weight: 400 !important;
}
.help-container {
padding-top: 2%;
text-align: center;
}
.blue {
color: #4285F4;
cursor: pointer;
}
mat-form-field {
width: 100%;
}
.user-name-container{
display: flex;
flex-direction: row;
gap: 3%;
}
.error-message {
font-weight: 500;
font-size: 0.8rem;
letter-spacing: -0.005rem;
color: #ed0c0c;
.mat-mdc-form-field-subscript-wrapper {
margin-top: -10px;
}
}
.forgot-password-container {
display: flex;
flex-direction: column;
text-align: center;
padding: 4%;
}
.forgot-password-icon-container {
width: 100%;
height: 100%;
mat-icon{
font-size: 105px;
height: 100%;
width: 100%;
padding: 1%;
padding-top: 7%;
color: #3467bb;
}
}
.forgot-password-heading-container {
font-size: 29px;
font-weight: 600;
font-family: 'Google Sans', sans-serif !important;
line-height: 2.5em;
}
.forgot-password-subtext-container{
display: flex;
flex-direction: column;
text-align: center;
font-size: 19px;
gap: 10px;
line-height: 1.5em;
mat-icon{
color: #4385f4;
}
}
.logo-icon {
height: 170px;
width: 100%;
}
.login-button {
width: 100%;
background: #4285F4;
box-sizing: border-box;
border: 1px solid #e0e0e0;
color: #fff;
position: relative;
user-select: none;
cursor: pointer;
outline: none;
-webkit-tap-highlight-color: transparent;
display: inline-block;
font-family: 'Google Sans', sans-serif !important;
font-size: 14px;
font-weight: 500;
white-space: nowrap;
text-decoration: none;
vertical-align: baseline;
text-align: center;
margin: 0;
min-width: 64px;
line-height: 44px;
padding: 0 16px;
border-radius: 4px;
overflow: visible;
transform: translate3d(0, 0, 0);
transition: background 400ms cubic-bezier(0.25, 0.8, 0.25, 1), box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1);
}
.register-text {
display: flex;
flex-direction: column;
align-items: flex-start;
margin-bottom: 0.3em;
font-weight: 400;
color: #5C5F61
}
.register-button {
width: 100%;
background: white;
box-sizing: border-box;
border: 1px solid #4285F4;
color: #4285F4;
position: relative;
user-select: none;
cursor: pointer;
outline: none;
-webkit-tap-highlight-color: transparent;
display: inline-block;
font-family: 'Google Sans', sans-serif !important;
font-size: 14px;
font-weight: 500;
white-space: nowrap;
text-decoration: none;
vertical-align: baseline;
text-align: center;
margin: 0;
min-width: 64px;
line-height: 44px;
padding: 0 16px;
border-radius: 4px;
overflow: visible;
transform: translate3d(0, 0, 0);
transition: background 400ms cubic-bezier(0.25, 0.8, 0.25, 1), box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1);
}
.send-email-button {
background: #40bd40 !important;
font-weight: 600 !important;
font-size: 16px !important;
}
.close-button {
float: right !important;
}
.forgot-password-link-container {
width: 100%;
text-align: end;
display: flex;
flex-direction: column;
padding-bottom: 4%;
z-index: 100;
position: relative;
}
.button-css {
display: flex;
justify-content: center;
.ok-button{
justify-content: center;
display: flex;
width: 25%;
background: #4285F4;
box-sizing: border-box;
border: 1px solid #e0e0e0;
color: #fff;
position: relative;
user-select: none;
cursor: pointer;
outline: none;
-webkit-tap-highlight-color: transparent;
font-family: 'Google Sans', sans-serif !important;
font-size: 14px;
font-weight: 500;
white-space: nowrap;
text-decoration: none;
vertical-align: baseline;
text-align: center;
margin: 0;
min-width: 64px;
line-height: 44px;
padding: 0 16px;
border-radius: 4px;
overflow: visible;
transform: translate3d(0, 0, 0);
transition: background 400ms cubic-bezier(0.25, 0.8, 0.25, 1), box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1);
}
}
.icon-css {
font-size: 40px;
color: #f3f30aed
}
.text-css {
font-size: 15px;
line-height: 1.45em;
font-family: "Google Sans", sans-serif !important;
color: #645a5a;
}
.logo-container {
display: flex;
flex-direction: row;
justify-content: center;
text-align: center;
mat-icon{
height: 20% !important;
width: 100% !important;
font-size: 70px !important;
color: #4285F4 !important;
}
}
.heading-span {
display: flex;
flex-direction: row;
gap: 8px;
align-items: center;
padding: 2%;
width: 100%;
justify-content: center;
font-size: 18px;
font-weight: 700;
padding-left: 15%;
color: #4285F4 !important;
}
.heading-container {
display: flex;
flex-direction: row;
justify-content: center;
background: aliceblue;
}
.sub-parent-container {
display: flex;
flex-direction: column;
text-align: center;
gap: 15px;
font-style: normal;
line-height: normal;
font-family: 'Google Sans', sans-serif !important;
}
.info {
padding-bottom: 10px !important;
small {
color: #a8a8b5;
}
}
@@ -0,0 +1,38 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {LoginComponent} from './login.component';
describe('LoginComponent', () => {
let component: LoginComponent;
let fixture: ComponentFixture<LoginComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [LoginComponent],
}).compileComponents();
fixture = TestBed.createComponent(LoginComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,92 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Component, NgZone, inject} from '@angular/core';
import {Auth, signInWithPopup, GoogleAuthProvider} from '@angular/fire/auth';
import {Router} from '@angular/router';
import {AuthService} from 'src/app/services/login/auth.service';
import {MatSnackBar} from '@angular/material/snack-bar';
import {environment} from 'src/environments/environment';
import {ToastMessageComponent} from '../toast-message/toast-message.component';
const HOME_ROUTE = '/';
interface LooseObject {
[key: string]: any;
}
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.scss'],
})
export class LoginComponent {
private readonly auth: Auth = inject(Auth);
private readonly provider: GoogleAuthProvider = new GoogleAuthProvider();
loader = false;
chatbotName: string = environment.chatbotName;
constructor(
private authService: AuthService,
private router: Router,
public ngZone: NgZone,
private _snackBar: MatSnackBar
) {
this.provider.setCustomParameters({
prompt: 'select_account',
});
}
loginWithGoogle() {
this.loader = true;
signInWithPopup(this.auth, this.provider)
.then((result: any) => {
const user = result.user.toJSON();
this.ngZone.run(() => {
this.authService.saveUserSession(user.stsTokenManager.accessToken);
this.redirect(user);
});
})
.catch(error => {
this.loader = false;
if (error.message !== 'Firebase: Error (auth/popup-closed-by-user).') {
this._snackBar.openFromComponent(ToastMessageComponent, {
panelClass: ['red-toast'],
verticalPosition: 'top',
horizontalPosition: 'right',
duration: 5000,
data: {
text: 'Error with SignIn. Please try again later !!!',
icon: 'cross-in-circle-white',
},
});
}
console.error(`Error: ${error}`);
});
}
redirect(user: any) {
const userDetails: LooseObject = {};
userDetails['name'] = user.displayName;
userDetails['email'] = user.email;
userDetails['photoURL'] = user.photoURL;
userDetails['domain'] = user.domain;
(userDetails['uid'] = user.uid),
localStorage.setItem('USER_DETAILS', JSON.stringify(userDetails));
this.loader = false;
this.router.navigate([HOME_ROUTE]);
}
}
@@ -0,0 +1,43 @@
<!--
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="chat-footer">
<div class="chat-textarea">
<mat-chip-grid class="intent-chip-textarea intent-chip"
style="width: 100%;" #chipGrid aria-label="Enter intent">
<input [matChipInputFor]="chipGrid"
placeholder="Ask your question here"
[(ngModel)]="term" (keydown.enter)="searchTerm()" />
</mat-chip-grid>
</div>
<div class="micIcon">
<button mat-icon-button (click)="startRecording()" matTooltip="Record"
*ngIf="!isRecording"><mat-icon>mic_none</mat-icon>
</button>
<span class="recording-container" *ngIf="isRecording">
<span class="blue-dot">&nbsp;</span>
<span class="green-dot">&nbsp;</span>
<span class="yellow-dot">&nbsp;</span>
<button (click)="stopRecording()" style="color: red;" class="reset-button initial" mat-icon-button
matTooltip="Stop Recording" aria-label="record button">
<mat-icon class="material-icons-outlined">stop_circle</mat-icon>
</button>
</span>
<button mat-icon-button (click)="searchTerm()">
<mat-icon>send</mat-icon>
</button>
</div>
</div>
@@ -0,0 +1,120 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.chat-footer {
margin-top: 1rem;
min-height: 60px;
display: flex;
border: 1px solid #dde6f6;
box-shadow: 0px 4px 18px rgba(195, 209, 226, 0.25);
border-radius: 30px;
width: 100%;
min-width: 720px;
// box-shadow: 5px 4px 24px rgba(66, 133, 244, 0.15);
.chat-textarea {
flex-grow: 1;
display: flex;
align-items: center;
padding-left: 2%;
input {
width: 100%;
height: 45px;
border: 0px;
border-radius: 4px;
resize: none;
outline: none;
padding: 14px;
font-family: "Google Sans", sans-serif !important;
box-sizing: border-box;
-ms-overflow-style: none;
scrollbar-width: none;
font-size: 0.95rem;
line-height: 1.1rem;
color: #534646;
}
input::-webkit-scrollbar {
display: none;
}
}
.micIcon {
display: flex;
align-items: center;
color: #1f1f1f;
margin-top: 2px;
padding-right: 1%;
img {
max-width: 40%;
cursor: pointer;
}
}
}
.recording-container {
display: flex;
flex-direction: row;
gap: 10px;
cursor: pointer;
align-items: baseline;
}
.blue-dot {
/* Ellipse 1226 */
content: '';
width: 8px;
height: 8px;
left: 1503px;
top: 445px;
display: flow-root;
border-radius: 15px;
/* Blue */
background: #4285F4;
animation: blue-transform 2s infinite;
}
.green-dot {
content: '';
width:8px;
height: 8px;
left: 1522px;
top: 445px;
display: flow-root;
border-radius: 15px;
/* Green */
background: #0F9D58;
animation: green-transform 2s infinite;
}
.yellow-dot {
content: '';
width: 8px;
height: 8px;
left: 1541px;
top: 445px;
display: flow-root;
border-radius: 15px;
/* Yellow */
background: #F4B400;
animation: yellow-transform 2s infinite;
}
@@ -0,0 +1,38 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {ChatInputComponent} from './chat-input.component';
describe('ChatInputComponent', () => {
let component: ChatInputComponent;
let fixture: ComponentFixture<ChatInputComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ChatInputComponent],
}).compileComponents();
fixture = TestBed.createComponent(ChatInputComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,86 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Component, EventEmitter, Output} from '@angular/core';
import {ActivatedRoute} from '@angular/router';
import {SpeechToTextService} from 'src/app/services/speech-to-text';
@Component({
selector: 'app-chat-input',
templateUrl: './chat-input.component.html',
styleUrls: ['./chat-input.component.scss'],
})
export class ChatInputComponent {
isRecording = false;
transcribedText = '';
mediaRecorder: MediaRecorder | undefined;
audioChunks: Blob[] = [];
term = '';
@Output() emitSearch: EventEmitter<string> = new EventEmitter();
constructor(
private speechToTextService: SpeechToTextService,
private route: ActivatedRoute
) {
const query = this.route.snapshot.queryParamMap.get('q');
if (query) {
this.term = query;
}
}
ngOnInit() {
navigator.mediaDevices
.getUserMedia({audio: true})
.then(stream => this.setupMediaRecorder(stream))
.catch(err => console.error(err));
}
searchTerm() {
this.emitSearch.emit(this.term);
}
setupMediaRecorder(stream: MediaStream) {
this.mediaRecorder = new MediaRecorder(stream);
this.mediaRecorder.ondataavailable = event =>
this.audioChunks.push(event.data);
this.mediaRecorder.onstop = () => this.sendAudioToGCP();
}
startRecording() {
this.isRecording = true;
this.audioChunks = [];
if (this.mediaRecorder) this.mediaRecorder.start();
}
stopRecording() {
this.isRecording = false;
if (this.mediaRecorder) this.mediaRecorder.stop();
}
async sendAudioToGCP() {
const audioBlob = new Blob(this.audioChunks);
// console.log(audioBlob);
(await this.speechToTextService.transcribeAudio(audioBlob)).subscribe(
(response: any) => {
// console.log(response)
this.term = response[0];
this.searchTerm();
},
(error: any) => console.error(error)
);
}
}
@@ -0,0 +1,32 @@
<!--
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<app-header></app-header>
<div class="parent-container">
<div class="wrapping-container">
<div class="main-div">
<div class="sub-heading">
<div class="name-container">
Hey, {{savedUser.name ? savedUser.name!.split(" ")[0]+'!' : ''}}
</div>
<div class="helper-container">How can I help you today?</div>
</div>
</div>
</div>
<div class="chat-footer-outer">
<app-chat-input (emitSearch)="this.goToResults($event)"></app-chat-input>
</div>
@@ -0,0 +1,995 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import url("https://fonts.googleapis.com/icon?family=Material+Icons|Material+Icons+Outlined");
:host ::ng-deep mat-form-feild .mat-form-field-appearance-outline .mat-form-field-outline-start {
color: red;
border-radius: 50px !important;
font-size: 10px !important;
}
:host ::ng-deep .mdc-text-field--outlined .mdc-notched-outline .mdc-notched-outline__leading {
border-radius: 28px 0 0 28px !important;
min-width: 28px !important;
border-color: #babfc4 !important;
}
:host ::ng-deep .mdc-text-field--outlined .mdc-notched-outline .mdc-notched-outline__trailing {
border-radius: 0 28px 28px 0 !important;
border-color: #babfc4 !important;
}
:host ::ng-deep .mat-form-field-outline-end {
border-radius: 0 28px 28px 0 !important;
}
:host ::ng-deep .mat-mdc-card {
box-shadow: 0px 2px 1px -1px rgb(217 220 224), 0px 1px 1px 0px rgba(0, 0, 0, 0.14), 0px 1px 3px 0px rgb(229 232 234) !important;
}
:host ::ng-deep .mat-mdc-standard-chip.mat-primary.mat-mdc-chip-selected,
.mat-mdc-standard-chip.mat-primary.mat-mdc-chip-highlighted {
--mdc-chip-elevated-container-color: #EDF3FE;
--mdc-chip-elevated-disabled-container-color: #e0e0e0;
--mdc-chip-label-text-color: #4285F4;
;
--mdc-chip-disabled-label-text-color: #4285F4;
;
--mdc-chip-with-icon-icon-color: #4285F4;
;
--mdc-chip-with-icon-disabled-icon-color: #4285F4;
;
--mdc-chip-with-trailing-icon-disabled-trailing-icon-color: #4285F4;
;
--mdc-chip-with-trailing-icon-trailing-icon-color: #4285F4;
;
--mdc-chip-with-icon-selected-icon-color: #4285F4;
;
}
.parent-container {
// background-position: bottom;
// background-image: url("../../../assets/images/footer.png");
// background-repeat: no-repeat;
// background-size: 30%;
// height: calc(100% - 60px);
// overflow: auto;
display: flex;
max-height: calc(100vh - 70px);
height: 100%;
font-family: 'Google Sans', sans-serif !important;
overflow-y: auto;
flex-direction: column;
.img-globe {
// position:absolute;
width: 66%;
}
}
.wrapping-container {
margin-top: 6%;
width: 100%;
padding-bottom: 1.5%;
}
.main-div {
display: flex;
justify-content: center;
align-items: center;
gap: 15px;
font-family: 'Google Sans', sans-serif !important;
}
.text-control {
width: 32%;
}
h1 {
line-height: 50px;
width: 34%;
}
h2 {
font-family: 'Google Sans', sans-serif !important;
}
.header {
-webkit-background-clip: text;
-webkit-text-fill-color: #48b975;
background-clip: text;
font-size: 3rem;
text-align: center;
font-weight: bold;
font-weight: 700;
-webkit-font-smoothing: antialiased;
height: 100%;
font-family: 'Google Sans', sans-serif;
}
.send-button {
color: #babfc4;
}
.logo-icon {
height: 120px;
width: 400px;
margin-bottom: 90px;
margin-left: -20px;
}
.user-info {
display: flex;
justify-content: center;
}
.user-info-column {
display: column;
}
.card-outer-div {
.card-inside-left {
.circle {
width: 100px;
height: 100px;
border: 5px solid rgb(255, 255, 255);
border-radius: 50%;
overflow: hidden;
box-shadow: 0px 2px 1px -1px rgba(0, 0, 0, 0.2), 0px 1px 1px 0px rgba(0, 0, 0, 0.14), 0px 1px 3px 0px rgba(0, 0, 0, 0.12);
}
.circle img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.card-inside-right {
width: 80%;
display: flex;
flex-direction: column;
align-items: center;
color: #747474;
font-size: 0.8rem;
font-weight: 500;
.name {
.title {
font: 1rem;
}
.sub-title {
font-size: 0.9rem;
margin-left: 2%;
}
}
.company {
display: flex;
align-items: center;
.logo {
padding-right: 1%;
}
.text {
width: 100%;
.sub-title {
margin-left: 2%;
}
}
}
}
}
.mat-mdc-standard-chip {
--mdc-chip-label-text-color: #4285F4;
--mdc-chip-elevated-container-color: #fff;
border: 1px solid #4285F4 !important;
font-family: "Google Sans", sans-serif !important;
font-size: 15px !important;
font-weight: 500 !important;
padding-left: 12px !important;
padding-right: 12px !important;
}
.sub-heading {
font-family: 'Google Sans', sans-serif;
width: 66%;
color: #161A1D;
text-align: center;
font-size: 30px;
line-height: 40px;
margin-top: -20px;
text-align: left;
z-index: 999;
}
.genAI {
//background-image: linear-gradient(to left, #4285F4, #DB4437, #F4B400, #0F9D58);
// background-image: linear-gradient(to left, #34A853, #FBBC05, #EA4335, #1A73E8);
background: linear-gradient(86.95deg, #34A853 -65.34%, #FBBC05 -12.75%, #EA4335 25.15%, #1A73E8 103.88%);
-webkit-background-clip: text;
-moz-background-clip: text;
background-clip: text;
color: transparent;
}
.margin-div {
margin-top: -20px;
}
.chat-footer-outer {
display: flex;
align-items: center;
width: 100%;
justify-content: center;
padding-bottom: 4.7%;
padding-top: 1.7%;
.chat-footer {
min-height: 60px;
display: flex;
border: 1px solid #DDE6F6;
box-shadow: 0px 4px 18px rgba(195, 209, 226, 0.25);
border-radius: 30px;
width: 66%;
// box-shadow: 5px 4px 24px rgba(66, 133, 244, 0.15);
.chat-textarea {
flex-grow: 1;
display: flex;
align-items: center;
padding-left: 2%;
input {
width: 100%;
height: 45px;
border: 0px;
border-radius: 4px;
resize: none;
outline: none;
padding: 14px;
font-family: 'Google Sans', sans-serif !important;
box-sizing: border-box;
-ms-overflow-style: none;
scrollbar-width: none;
font-size: 0.95rem;
line-height: 1.1rem;
color: #534646;
}
input::-webkit-scrollbar {
display: none;
}
}
.micIcon {
display: flex;
align-items: center;
color: #1F1F1F;
margin-top: 2px;
padding-right: 1%;
img {
max-width: 40%;
cursor: pointer;
}
}
}
}
.text-gnosis {
color: var(--White, #FFF);
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 24px;
/* 150% */
letter-spacing: 0.4px;
}
.chip-cancel-button {
color: #002dff;
}
.categories-div {
.mat-mdc-standard-chip {
--mdc-chip-label-text-color: #0F9D58;
--mdc-chip-elevated-container-color: #fff;
border: 1px solid #0F9D58 !important;
font-family: 'Google Sans', sans-serif !important;
// font-weight: 600;
}
}
.intent-chip {
.mat-mdc-standard-chip {
--mdc-chip-label-text-color: #4285F4;
--mdc-chip-elevated-container-color: #fff;
border: 1px solid #4285F4 !important;
font-family: 'Google Sans', sans-serif !important;
// font-weight: 600;
}
.chip-cancel-button {
color: #002dff;
}
}
.category-chip {
.mat-mdc-standard-chip {
--mdc-chip-label-text-color: #0F9D58;
--mdc-chip-elevated-container-color: #fff;
border: 1px solid #0F9D58 !important;
font-family: 'Google Sans', sans-serif !important;
// font-weight: 600;
}
.chip-cancel-button {
color: #007d06;
}
}
.questions-div {
flex-direction: column;
margin-top: 2rem;
margin-bottom: 5rem;
.intent-card {
padding: 0px;
}
.intent-heading {
color: #787978;
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: normal;
font-family: 'Google Sans', sans-serif !important;
width: 66%;
}
.intent-container {
border-radius: 8px;
background-color: rgba(246, 249, 254, 1);
width: 66%;
box-shadow: none !important;
flex-shrink: 0;
.selected-question {
// margin: 0.5rem 0rem;
&:hover {
background-color: rgba(66, 133, 244, 0.10);
}
}
.question-item {
display: flex;
justify-content: space-between;
font-family: 'Google Sans', sans-serif !important;
color: #787978;
font-size: 1em;
line-height: 24px;
}
.question-link {
overflow: hidden;
text-overflow: ellipsis;
}
}
}
.intent-chip-textarea ::ng-deep .mdc-evolution-chip-set__chips {
align-items: center !important;
}
.float-button {
position: fixed;
right: 3em;
bottom: 6em;
--mdc-fab-container-color: #4285F4;
}
.name-container {
font-size: 3.5rem;
font-weight: 400;
line-height: 4rem;
font-family: Google Sans, Helvetica Neue, sans-serif;
letter-spacing: -.03em;
background: linear-gradient(89.9deg, rgba(66, 133, 244, 0.95) 0.17%, #A488F5 7.34%, rgba(234, 67, 53, 0.88) 24.26%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-fill-color: transparent;
animation: effect 2s linear;
display: flex;
flex-direction: row;
gap: 1%;
align-items: center;
justify-content: center;
}
@keyframes effect {
0% {
background: linear-gradient(89.9deg, rgba(240, 216, 214, 0.88) 0.17%, #ebcaec 7.34%, rgba(194, 214, 247, 0.95) 24.26%);
-webkit-background-clip: text;
}
10% {
background: linear-gradient(89.9deg, rgba(227, 237, 253, 0.95) 0.17%, #e5ddfd 7.34%, rgba(186, 210, 248, 0.95) 24.26%);
-webkit-background-clip: text;
}
20% {
background: linear-gradient(89.9deg, rgba(218, 231, 253, 0.95) 0.17%, #dcd1fc 7.34%, rgba(172, 202, 252, 0.95) 24.26%);
-webkit-background-clip: text;
}
30% {
background: linear-gradient(89.9deg, rgba(199, 219, 252, 0.95) 0.17%, #d0c2fa 7.34%, rgba(161, 196, 252, 0.95) 24.26%);
-webkit-background-clip: text;
}
40% {
background: linear-gradient(89.9deg, rgba(169, 201, 253, 0.95) 0.17%, #cdbcff 7.34%, rgba(149, 188, 250, 0.95) 24.26%);
-webkit-background-clip: text;
}
50% {
background: linear-gradient(89.9deg, rgba(200, 220, 253, 0.95) 0.17%, #bba5fc 7.34%, rgba(203, 221, 250, 0.95) 24.26%);
-webkit-background-clip: text;
}
60% {
background: linear-gradient(89.9deg, rgba(227, 237, 253, 0.95) 0.17%, #e5ddfd 7.34%, rgba(186, 210, 248, 0.95) 24.26%);
-webkit-background-clip: text;
}
70% {
background: linear-gradient(89.9deg, rgba(183, 210, 253, 0.95) 0.17%, #e5ddfd 7.34%, rgba(248, 217, 215, 0.88) 24.26%);
-webkit-background-clip: text;
}
80% {
background: linear-gradient(89.9deg, rgba(122, 172, 252, 0.95) 0.17%, #e5ddfd 7.34%, rgba(247, 182, 176, 0.88) 24.26%);
-webkit-background-clip: text;
}
90% {
background: linear-gradient(89.9deg, rgba(94, 148, 236, 0.95) 0.17%, #b49ff1 7.34%, rgba(240, 121, 111, 0.88) 24.26%);
-webkit-background-clip: text;
}
100% {
background: linear-gradient(89.9deg, rgba(66, 133, 244, 0.95) 0.17%, #A488F5 7.34%, rgba(234, 67, 53, 0.88) 24.26%);
-webkit-background-clip: text;
}
}
.helper-container {
white-space: nowrap;
overflow: hidden;
font-size: 3.5rem;
font-weight: 400;
line-height: 4rem;
font-family: Google Sans, Helvetica Neue, sans-serif;
letter-spacing: -.03em;
color: #8f9391;
padding-bottom: 1%;
opacity: 0.5;
animation: typing 2s steps(22), blink .5s step-end infinite alternate;
justify-content: center;
display: flex;
}
@keyframes typing {
from {
width: 0
}
}
@keyframes blink {
50% {
border-color: transparent
}
}
.quick-bot-introduction-container {
width: 100%;
display: flex;
flex-direction: row;
justify-content: center;
.introduction-sub-container {
width: 66%;
justify-content: center;
text-align: center;
display: flex;
flex-direction: column;
padding-top: 2%;
padding-bottom: 2%;
background: radial-gradient(white, #FAFCFF) padding-box, linear-gradient(89.86deg, #34A853 -40.93%, #FBBC05 4.92%, #EA4335 37.97%, #1A73E8 106.62%) border-box;
border-radius: 24px;
border: 1px solid transparent;
}
.logo-image-container {
img {
height: 110px;
width: 110px;
}
}
.logo-text-heading {
font-style: normal;
font-weight: 500;
font-size: 28px;
line-height: 202%;
/* or 65px */
text-align: center;
background: linear-gradient(89.86deg, #34A853 -40.93%, #FBBC05 4.92%, #EA4335 37.97%, #1A73E8 106.62%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-fill-color: transparent;
}
.logo-text-subheading {
font-style: normal;
font-weight: 500;
font-size: 21px;
line-height: 202%;
/* or 48px */
text-align: center;
}
.blue {
color: #4285F4;
}
}
.helpButton {
position: fixed !important;
bottom: 45px !important;
right: 20px !important;
background-color: #4285f4 !important;
color: white !important;
text-decoration: none !important;
border-radius: 60px !important;
height: 41px !important;
display: inline-flex !important;
align-items: center !important;
overflow: hidden !important;
width: auto !important;
max-width: 41px !important;
-webkit-transition: max-width 0.5s !important;
transition: max-width 0.5s !important;
z-index: 1000 !important;
&:hover {
max-width: 300px !important;
}
.text {
white-space: nowrap !important;
padding-right: 15px !important;
font-size: 15px !important;
font-weight: 600 !important;
}
}
::placeholder {
color: #646566;
}
.intent-container-box {
box-sizing: border-box;
width: 239px;
height: 239px;
border-radius: 14px;
transition-delay: 2s;
transition: width 8s;
padding: 1%;
cursor: pointer;
}
.selected-intent-box {
box-sizing: border-box;
height: 239px;
border-radius: 14px;
transition-delay: 2s;
transition: width 8s;
cursor: pointer;
width: max-content !important;
max-width: 100%;
display: flex;
flex-direction: column;
gap: 1%;
padding: 1%;
background: linear-gradient(92.1deg, #bdd1f3 19.16%, rgb(199 183 245 / 90%) 79%, rgb(235 165 158 / 90%) 135.24%), linear-gradient(272.04deg, rgba(158, 188, 237, 0.7) 14.93%, rgba(158, 188, 237, 0) 104.51%) !important;
}
.cards-outer-container {
width: 65%;
overflow: auto;
scrollbar-width: none;
}
.cards-outer-container:after {
z-index: -1;
content: '';
width: 10%;
opacity: 0.5 !important;
}
.cards-container-all-categories {
width: max-content;
display: flex;
overflow-x: auto;
gap: 1%;
max-width: 160%;
min-width: 100%;
scrollbar-width: none;
}
.cards-container-few-categories {
width: max-content;
display: flex;
overflow-x: auto;
gap: 1%;
max-width: 160%;
min-width: 100%;
scrollbar-width: none;
justify-content: center;
padding-inline: 5%;
}
.intent-container-box:hover {
background: #DDE3EA !important;
}
.card-detail-container {
width: 100%;
min-width: 250px;
display: flex;
flex-direction: row;
}
.selected-intent-suggested-question {
gap: 4%;
display: flex !important;
flex-direction: column;
}
.suggested-question-chip-row {
margin-top: 0.5rem;
.suggested-question-chip {
display: flex;
flex-direction: row;
margin: 0.5rem;
padding: 0.5rem;
width: 100%;
.suggested-question-icon {
padding-right: 0.25rem
}
.suggested-question-text {
margin-left: 0.25rem;
width: 100%;
}
}
}
.close-button-container {
display: none !important;
}
.expand-close-button-container {
display: block !important;
.close-button {
margin-top: -10px !important;
}
}
.card-heading {
padding-top: 2.5%;
font-size: 17px;
color: #1F1F1F;
width: 90%;
}
.card-content {
font-size: 13px;
color: #646566;
padding-top: 4%;
white-space: break-spaces;
text-wrap: balance;
padding-right: 3%;
text-align: left;
width: 85%;
}
::ng-deep .mat-mdc-snack-bar-container {
&.green-snackbar {
--mdc-snackbar-container-color: #0F9D58;
--mat-mdc-snack-bar-button-color: #fff;
--mdc-snackbar-supporting-text-color: #fff;
}
&.red-snackbar {
--mdc-snackbar-container-color: #e9103f;
--mat-mdc-snack-bar-button-color: #fff;
--mdc-snackbar-supporting-text-color: #fff;
}
}
::ng-deep .mat-mdc-dialog-surface {
justify-content: center !important;
display: flex !important;
flex-direction: column !important;
text-align: center !important;
background: #FFFFFF;
border-radius: 20px !important;
box-sizing: border-box;
box-shadow: none !important;
.badge-prompt-parent-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
.badge-greetings-container {
width: 100%;
display: flex;
flex-direction: row;
justify-content: center;
img {
width: 45%;
}
}
.badge-text-container {
font-style: normal;
font-weight: 400;
font-size: 17px;
line-height: 45px;
display: flex;
align-items: center;
color: rgba(92, 95, 97, 0.7);
}
}
}
.blue-dot {
/* Ellipse 1226 */
content: '';
width: 8px;
height: 8px;
left: 1503px;
top: 445px;
display: flow-root;
border-radius: 15px;
/* Blue */
background: #4285F4;
animation: blue-transform 2s infinite;
}
.green-dot {
content: '';
width:8px;
height: 8px;
left: 1522px;
top: 445px;
display: flow-root;
border-radius: 15px;
/* Green */
background: #0F9D58;
animation: green-transform 2s infinite;
}
.yellow-dot {
content: '';
width: 8px;
height: 8px;
left: 1541px;
top: 445px;
display: flow-root;
border-radius: 15px;
/* Yellow */
background: #F4B400;
animation: yellow-transform 2s infinite;
}
.recording-container {
display: flex;
flex-direction: row;
gap: 10px;
cursor: pointer;
align-items: baseline;
}
@keyframes blue-transform {
0% {
transform: translateY(-14px)
}
33% {
transform: translateY(0)
}
66% {
transform: translateY(0)
}
100% {
transform: translateY(-14px)
}
}
@keyframes green-transform {
0% {
transform: translateY(0)
}
33% {
transform: translateY(-14px)
}
66% {
transform: translateY(0)
}
100% {
transform: translateY(0)
}
}
@keyframes yellow-transform {
0% {
transform: translateY(0)
}
33% {
transform: translateY(0)
}
66% {
transform: translateY(-14px)
}
100% {
transform: translateY(0)
}
}
.tooltip-container {
display: flex;
width: 100%;
justify-content: center;
align-items: baseline;
padding-top: 2%;
padding-bottom: 2%;
.tooltip-sub-container {
width: 66%;
display: flex;
align-items: baseline;
justify-content: center;
}
}
.tooltip-icon-container {
display: flex;
flex-direction: row;
// align-self: flex-end;
gap: 10px;
flex-wrap: nowrap;
text-wrap: nowrap;
align-self: center;
color: #5F6368;
.tooltip-label {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
font-size: 16px;
line-height: 20px;
text-align: center;
}
mat-icon {
font-size: 22px !important;
}
}
.tooltip-text-container {
flex-wrap: wrap;
word-wrap: break-word;
justify-content: flex-start;
display: flex;
color: #5F6368;
.tooltip-text {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
font-size: 16px;
line-height: 20px;
}
}
.dismiss-container {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
font-size: 14px;
line-height: 20px;
/* identical to box height, or 143% */
text-align: center;
/* Blue */
color: #4285F4;
padding-left: 1%;
cursor: pointer;
}
.dismiss-container:hover {
text-decoration: underline;
}
.journey-chips {
max-height: 300px;
overflow-y: auto;
}
::ng-deep .mat-mdc-standard-chip:hover {
background: linear-gradient(92.1deg, #9EBCED 19.16%, rgba(164, 136, 245, 0.9) 79%) !important;
--mdc-chip-label-text-color: #fff;
}
.suggested-question-container {
width: 60%;
}
@@ -0,0 +1,38 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {MainComponent} from './main.component';
describe('MainComponent', () => {
let component: MainComponent;
let fixture: ComponentFixture<MainComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [MainComponent],
}).compileComponents();
fixture = TestBed.createComponent(MainComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,76 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Component, OnDestroy} from '@angular/core';
import {UserService} from 'src/app/services/user/user.service';
import {Router} from '@angular/router';
import {MatDialog} from '@angular/material/dialog';
import {ReplaySubject} from 'rxjs';
import {takeUntil} from 'rxjs/operators';
import {SearchResponse} from 'src/app/models/search.model';
import {SearchApplicationFormComponent} from '../search-application/search-application-form/search-application-form.component';
import {SearchApplicationService} from 'src/app/services/search_application.service';
@Component({
selector: 'app-main',
templateUrl: './main.component.html',
styleUrls: ['./main.component.scss'],
})
export class MainComponent implements OnDestroy {
private readonly destroyed = new ReplaySubject<void>(1);
term = '';
showResults = false;
searchResults: SearchResponse | undefined;
savedUser;
constructor(
public userService: UserService,
private router: Router,
public dialog: MatDialog,
private readonly searchApplicationService: SearchApplicationService
) {
this.savedUser = userService.getUserDetails();
this.checkConfiguration();
}
checkConfiguration() {
this.searchApplicationService
.get()
.pipe(takeUntil(this.destroyed))
.subscribe(searchApplication => {
if (!searchApplication) {
this.openAddAgentBuilderConfigForm();
}
});
}
goToResults(term: string) {
this.router.navigate(['/search'], {queryParams: {q: term}});
}
openAddAgentBuilderConfigForm() {
this.dialog.open(SearchApplicationFormComponent, {
disableClose: true,
height: '600px',
width: '1120px',
});
}
ngOnDestroy() {
this.destroyed.next();
this.destroyed.complete();
}
}
@@ -0,0 +1,73 @@
<!--
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<app-header (emitSearch)="this.searchTerm($event)"></app-header>
<div class="overview-container" *ngIf="summary">
<div class="overview-sub-container">
<div class="overview-text"><mat-icon>auto_awesome</mat-icon> AI Overview</div>
<div class="overview-content"> {{summary}}</div>
<div class="overview-button">
<button mat-button>Learn more</button>
</div>
</div>
</div>
<div class="overview-container padding-top">
<div class="search-result-parent-container" *ngIf="documents.length>0">
<div class="documents-container">
<div class="header-container">
Documents
</div>
<div class="navigation">
<button class="nav-button" (click)="prevPage()" [disabled]="currentPage === 0">
<span class="material-symbols-outlined">chevron_left</span>
</button>
<button class="nav-button" (click)="nextPage()" [disabled]="currentPage >= totalPages -1">
<span class="material-symbols-outlined">chevron_right</span>
</button>
</div>
</div>
<div class="card-list">
<div class="card" *ngFor="let result of pagedDocuments" (click)="openNewWindow(result.link)">
<div class="card-content">
<div class="card-header">
<img src="{{result.link.includes(pdf) ? imageName.get('pdf'): imageName.get('doc')}}" height="30" width="30"/>
<span class="card-title">{{ result.title | truncate }}</span>
</div>
<div class="card-description"><p>
{{ result.content | truncate }}
</p></div>
<div class="overview-button">
<button mat-button (click)="previewDocument($event, result)">Preview</button>
</div>
</div>
</div>
</div>
</div>
<div *ngIf="this.selectedDocument !== undefined" class="document-viewer">
<div class="document-viewer-header">
<div class="card-header">
<img src="{{this.selectedDocument.link.endsWith(pdf) ? imageName.get('pdf'): imageName.get('doc')}}" height="30" width="30"/>
<span class="card-title">{{ this.selectedDocument.title| truncate }}</span>
</div>
<div class="document-viewer-header-close">
<button (click)="closePreview()" mat-icon-button value="">
<mat-icon aria-hidden="true">close</mat-icon>
</button>
</div>
</div>
<iframe class="document-viewer-content" [src]="this.safeUrl" name="preview"></iframe>
</div>
@@ -0,0 +1,296 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.overview-container {
display: flex;
justify-content: center;
padding-top: 2%;
padding-right: 1.5%;
.overview-sub-container {
width: 66%;
background: linear-gradient(92.1deg, #9ebcedb2 19.16%, rgba(163, 136, 245, 0.651) 79%, rgba(234, 68, 53, 0.493) 135.24%);
border-radius: 12px;
.overview-text {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
font-size: 20px;
line-height: 24px;
color: #5C5F61;
padding: 2%;
}
.overview-content {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
font-size: 19px;
line-height: 30px;
color: #161A1D;
padding-left: 2%;
padding-right: 2%;
padding-bottom: 2%;
}
.overview-button {
button {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
font-size: 20px;
line-height: 24px;
color: #4285F4;
padding: 40px;
}
}
}
}
.header-container {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
font-size: 16px;
line-height: 19px;
color: #5F6368;
display: flex;
justify-content: flex-start;
width: 66%;
}
.search-result:hover {
border: 1px solid #DFE0E2;
border-radius: 10px;
}
.search-result-parent-container{
display: flex;
flex-direction: column;
width: 66%;
gap: 15px;
padding-bottom: 5%;
}
.search-result-row-container {
display: flex;
flex-direction: row;
gap: 15px;
}
.search-result {
padding: 1%;
padding-top: 2%;
cursor: pointer;
border: 1px solid transparent;
width: 25%;
.title-container {
display: flex;
flex-direction: row;
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
font-size: 16px;
line-height: 31px;
color: #161A1D;
display: flex;
gap: 15px;
.link-container {
font-size: 14px !important;
font-weight: 400 !important;
color: #676c6f !important;
}
}
.description-container {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
font-size: 16px;
line-height: 24px;
color: #161A1D;
opacity: 0.8;
padding: 1%;
padding-top: 4%;
}
.overview-button {
padding-top: 2%;
button {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
font-size: 16px;
line-height: 24px;
color: #4285F4;
}
}
}
.padding-top {
padding-top: 0;
}
.close-container {
display: flex;
justify-content: end;
}
.pdf-display{
display: flex;
justify-content: center;
}
.overview {
z-index: 1000;
}
/* card-list.component.scss */
.card-list {
display: flex;
gap: 20px; /* Adjust spacing between cards */
}
.card {
border: 1px solid #eee;
border-radius: 8px;
flex: 0 0 auto; /* Prevent cards from stretching */
box-shadow: 2px 2px 5px rgba(0,0,0,0.1);
padding: 16px;
transition: transform 0.2s;
&:hover {
transform: scale(1.02);
box-shadow: 3px 3px 7px rgba(0,0,0,0.15);
cursor: pointer;
}
.card-content {
display: flex;
flex-direction: column;
height: 100%;
}
.card-header {
display: flex;
align-items: center;
margin-bottom: 10px;
.material-symbols-outlined {
font-size: 1.2rem;
margin-right: 5px;
&.bookmark {
margin-left: auto; // Push bookmark to right
cursor: pointer;
}
}
.card-title {
font-weight: 500;
width: 250px;
}
}
.card-description {
flex-grow: 1; // Allow description to take up available space
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 10; /* Number of lines to show */
-webkit-box-orient: vertical;
width: 250px;
}
.card-footer {
display: flex;
align-items: center;
margin-top: 10px;
.material-symbols-outlined {
font-size: 1.2rem;
margin-right: 5px;
}
}
}
.documents-container {
margin-top: 2rem;
display: flex;
justify-content: space-between;
align-items: center;
max-width: 1000px;
}
.navigation {
display: flex;
justify-content: center;
margin-top: 20px;
.nav-button {
border: none;
background: none;
cursor: pointer;
transition: background-color 0.2s;
&:hover {
background-color: #f0f0f0;
}
&:disabled {
opacity: 0.5;
cursor: default;
}
.material-symbols-outlined {
font-size: 1.5rem;
}
}
}
.document-viewer {
background: rgb(255, 255, 255);
border-left: 1px solid var(--md-sys-color-outline);
border-top: 1px solid var(--md-sys-color-outline);
display: block;
height: 75%;
position: fixed;
right: 0px;
bottom: 0px;
width: 35%;
z-index: 9999;
.document-viewer-header {
-webkit-box-pack: justify;
-webkit-box-align: center;
-webkit-box-flex: 1;
align-items: center;
border-bottom: 1px solid var(--md-sys-color-outline);
display: flex;
flex: 1 1 0%;
justify-content: space-between;
padding: 8px;
}
.document-viewer-content {
width: 100%;
height: 100%;
}
}
@@ -0,0 +1,38 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {SearchResultsComponent} from './search-results.component';
describe('SearchResultsComponent', () => {
let component: SearchResultsComponent;
let fixture: ComponentFixture<SearchResultsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [SearchResultsComponent],
}).compileComponents();
fixture = TestBed.createComponent(SearchResultsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,139 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Component, OnDestroy, ViewChild, TemplateRef} from '@angular/core';
import {SearchService} from 'src/app/services/search.service';
import {ReplaySubject} from 'rxjs';
import {UserService} from 'src/app/services/user/user.service';
import {ActivatedRoute, Router} from '@angular/router';
import {search_image_type, PDF, image_name} from 'src/environments/constant';
import {
DomSanitizer,
SafeResourceUrl,
SafeUrl,
} from '@angular/platform-browser';
import {MatDialog} from '@angular/material/dialog';
@Component({
selector: 'app-search-results',
templateUrl: './search-results.component.html',
styleUrls: ['./search-results.component.scss'],
})
export class SearchResultsComponent implements OnDestroy {
@ViewChild('preview', {static: true})
previewRef!: TemplateRef<{}>;
summary = '';
private readonly destroyed = new ReplaySubject<void>(1);
serachResult: any = [];
documents: any = [];
images: any = [];
pdf = PDF;
imageName = image_name;
documentURL: SafeResourceUrl | undefined;
openPreviewDocument: any;
currentPage = 0;
pageSize = 3;
selectedDocument: any;
safeUrl: SafeUrl | undefined;
constructor(
private router: Router,
private route: ActivatedRoute,
private service: SearchService,
private userService: UserService,
private dialog: MatDialog,
private sanitizer: DomSanitizer
) {
const query = this.route.snapshot.queryParamMap.get('q');
this.service.search(query!).subscribe({
next: (searchRespone: any) => {
this.summary = searchRespone.summary;
this.serachResult = searchRespone.results;
this.serachResult.forEach((element: any) => {
this.documents.push(element);
if (search_image_type.includes(element.link.split('.')[1])) {
this.images.push(element);
}
});
console.log(this.documents, this.images);
this.userService.hideLoading();
},
error: () => {
this.userService.hideLoading();
},
});
}
searchTerm(term: string) {
this.router.navigate(['/search'], {queryParams: {q: term}});
this.service.search(term).subscribe({
next: (searchRespone: any) => {
this.serachResult = searchRespone.results;
this.summary = searchRespone.summary;
this.userService.hideLoading();
},
error: () => {
this.userService.hideLoading();
},
});
}
openNewWindow(link: string) {
window.open(link, '_blank');
}
previewDocument(event: any, document: any) {
event.stopPropagation();
if (document.link.endsWith('.pdf') || document.link.endsWith('.docx')) {
this.selectedDocument = document;
this.safeUrl = this.sanitizer.bypassSecurityTrustResourceUrl(
this.selectedDocument.link
);
}
}
closePreview() {
this.selectedDocument = undefined;
}
ngOnDestroy() {
this.destroyed.next();
this.destroyed.complete();
}
get pagedDocuments() {
const startIndex = this.currentPage * this.pageSize;
return this.documents.slice(startIndex, startIndex + this.pageSize);
}
get totalPages() {
return Math.ceil(this.documents.length / this.pageSize);
}
nextPage() {
if (this.currentPage < this.totalPages - 1) {
this.currentPage++;
}
}
prevPage() {
if (this.currentPage > 0) {
this.currentPage--;
}
}
}
@@ -0,0 +1,74 @@
<!--
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="buttons-container">
<button mat-raised-button class="inactive-button" (click)="navigateToMain()">
<mat-icon>keyboard_backspace</mat-icon>
Back
</button>
</div>
<div class="root-container">
<div class="max-width-container">
<div class="buttons-container">
<div class="heading-container">
<span>Manage Agent Builder Config</span>
</div>
</div>
<div class="table-container">
<form>
<mat-form-field
class="form-field"
appearance="outline"
[floatLabel]="'always'"
>
<mat-label>Choose an Engine</mat-label>
<mat-select [disabled]="!this.editMode" [(ngModel)]="selectedEngine" name="engine">
<mat-option *ngFor="let engine of this.engines" [value]="engine">{{
engine.name
}}</mat-option>
</mat-select>
</mat-form-field>
<div class="intent-buttons-container">
<button
mat-raised-button
class="reset-button"
*ngIf="!this.editMode"
(click)="enableForm()"
>
<mat-icon>edit</mat-icon>
Edit
</button>
<button
mat-raised-button
class="reset-button"
*ngIf="this.editMode"
(click)="disableForm()"
>
<span> Discard Changes </span>
</button>
<button
mat-raised-button
class="reset-button"
*ngIf="this.editMode"
(click)="saveForm()"
>
<span>Save</span>
</button>
</div>
</form>
</div>
</div>
</div>
@@ -0,0 +1,571 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.root-container {
display: flex;
width: 100%;
justify-content: center;
.max-width-container {
width: 100%;
max-width: 1280px;
}
}
.action-button-container {
padding: 0;
}
.table-container {
justify-content: center;
display: block;
flex-direction: row;
padding: 5.5% 3%;
padding-bottom: 5%;
.row {
display: flex;
flex-direction: row;
.mat-mdc-form-field {
width: 100% !important;
}
}
}
.amber {
color: #fbc645;
}
.red {
color: red;
}
.blue {
color: #4285F4;
}
::ng-deep .mat-checkbox-checked .mat-checkbox-background,
.mat-checkbox-indeterminate .mat-checkbox-background {
background-color: #4285F4 !important;
}
// overwrite the ripple overlay on hover and click
::ng-deep .mat-checkbox:not(.mat-checkbox-disabled) .mat-checkbox-ripple .mat-ripple-element {
background-color: #4285F4 !important;
}
::ng-deep .mdc-checkbox__ripple {
background-color: #4285F4 !important;
}
::ng-deep .mat-mdc-checkbox .mdc-checkbox .mdc-checkbox__native-control:enabled:checked~.mdc-checkbox__background {
background-color: #4285F4 !important;
border-color: #4285F4 !important;
}
::ng-deep .mat-mdc-checkbox .mdc-checkbox .mdc-checkbox__native-control:enabled:indeterminate~.mdc-checkbox__background {
background-color: #4285F4 !important;
border-color: #4285F4 !important;
}
::ng-deep .mat-mdc-slide-toggle.mat-mdc-slide-toggle-checked:not(.mat-disabled) .mdc-switch__shadow {
background-color: #4285F4;
/*replace with your color*/
}
::ng-deep .mat-mdc-slide-toggle.mat-mdc-slide-toggle-checked:not(.mat-disabled) .mdc-switch__track::after {
background-color: #4285F4 !important;
/*replace with your color*/
}
.buttons-container {
display: flex;
flex-direction: row;
justify-content: flex-start;
width: 100%;
padding-top: 1%;
padding-bottom: 1%;
padding-left: 3%;
}
.saved-intents-container {
padding-top: 2%;
padding-bottom: 2%;
}
.heading-container {
width: 73%;
align-content: center;
flex-wrap: wrap;
display: flex;
flex-direction: row;
span {
font-family: 'Google Sans', sans-serif !important;
font-style: normal;
font-weight: 400;
font-size: 20px;
line-height: 14px;
color: #161A1D;
}
}
.button-child-container {
display: flex;
flex-direction: row;
gap: 20px;
width: 24%;
justify-content: center;
button {
border-radius: 30px;
font-family: 'Google Sans', sans-serif !important;
font-style: normal;
font-weight: 500;
font-size: 14px;
line-height: 23px;
letter-spacing: unset;
box-shadow: none;
}
.bulk-delete-button {
color: #f70808;
border: 1px solid #f70808;
}
.create-notification-button {
color: #fff;
background: #4285F4;
}
}
.form-field {
width: 100%;
}
.dialog-toggle-button-container {
font-family: 'Google Sans', sans-serif !important;
font-style: normal;
width: 100%;
display: flex;
flex-direction: row;
gap: 10px;
padding-bottom: 4%;
button {
height: 45px;
border: none;
}
.error {
background: #e9867e33;
width: 28%;
}
.warning {
background: rgb(241 226 183 / 44%);
width: 28%;
}
.info {
background: #F6F9FE;
width: 35%;
}
.errorOutline {
border: 1px solid red;
}
.warningOutline {
border: 1px solid #fbc645;
}
.infoOutline {
border: 1px solid #4285F4;
}
}
.delete-confirmation-text {
font-family: 'Google Sans', sans-serif !important;
font-style: normal;
width: 100%;
display: flex;
justify-content: center;
font-size: 20px;
color: #161A1D;
}
.dialog-delete-button {
border-radius: 30px;
font-family: 'Google Sans', sans-serif !important;
font-style: normal;
font-weight: 500;
font-size: 14px;
line-height: 23px;
letter-spacing: unset;
box-shadow: none;
color: #f70808;
border: 1px solid #f70808;
}
.disableDiv {
pointer-events: none;
}
.spinner-container {
width: 100%;
justify-content: center;
display: flex;
flex-direction: row;
text-align: center;
position: absolute;
top: 50%;
}
.spinner-dialog-container {
width: 100%;
justify-content: flex-end;
display: flex;
flex-direction: row;
padding-right: 5%;
padding-bottom: 4%;
}
.text-column {
width: 30% !important;
word-break: break-word;
}
.comment-detail-container {
display: flex;
flex-direction: column;
gap: 20px;
}
.details-container {
font-weight: 600;
border-radius: 5px;
border: 1px solid #9e9e9e;
text-align: start;
padding: 3%;
gap: 14px;
display: flex;
flex-direction: column;
.question-details-container {
background: white;
font-weight: 400;
border-radius: 12px;
}
}
.chip-color {
background: aliceblue !important;
border: 1px solid #4285f4;
}
.action-column {
width: 8%;
}
::-webkit-scrollbar {
width: 9px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background-color: rgba(155, 155, 155, 0.5);
border-radius: 20px;
border: transparent;
}
.search-question-field {
width: 100%;
mat-icon {
color: #363e45;
cursor: pointer;
}
}
::ng-deep .search-question-field .mat-mdc-text-field-wrapper.mdc-text-field--outlined .mat-mdc-form-field-infix {
padding-top: 10px !important;
padding-bottom: 10px !important;
}
::ng-deep .search-question-field .mdc-text-field__input {
height: 34px !important;
}
.input-field-container {
display: flex;
flex-direction: row;
gap: 2%;
align-items: center;
}
.submit-button {
// width: 8% !important;
background: #4285F4 !important;
border: 1px solid #4285F4;
box-sizing: border-box;
// border: 1px solid #e0e0e0 !important;
color: #fff !important;
position: relative;
user-select: none;
cursor: pointer;
outline: none;
-webkit-tap-highlight-color: transparent;
font-family: 'Google Sans', sans-serif !important;
font-size: 14px;
font-weight: 500;
white-space: nowrap;
text-decoration: none;
vertical-align: baseline;
text-align: center;
margin: 0;
min-width: 64px;
line-height: 36px;
padding: 20px 46px;
border-radius: 48px !important;
overflow: visible;
transform: translate3d(0, 0, 0);
transition: background 400ms cubic-bezier(0.25, 0.8, 0.25, 1), box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1);
}
.reset-button {
background: white !important;
box-sizing: border-box !important;
border: 1px solid #4285F4 !important;
color: #4285F4 !important;
cursor: pointer;
outline: none;
font-family: 'Google Sans', sans-serif !important;
font-size: 14px;
font-weight: 500;
margin: 0;
min-width: 130px;
line-height: 36px;
padding: 0px 25px;
height: 100%;
border-radius: 48px !important;
display: flex;
justify-content: center;
align-content: center;
align-items: center;
flex-direction: row;
}
.inactive-button {
background: #ffffff !important;
box-sizing: border-box !important;
border: 1px solid rgba(43, 41, 40, 0.502) !important;
color: rgba(43, 41, 40, 0.93) !important;
cursor: pointer;
outline: none;
font-family: "Google Sans", sans-serif !important;
font-size: 14px;
font-weight: 500;
margin: 0;
min-width: 64px;
line-height: 36px;
padding: 0px 45px;
height: 60%;
margin-left: 2rem;
border-radius: 48px !important;
display: flex;
justify-content: center;
flex-direction: row;
flex-wrap: wrap;
align-content: center;
align-items: center;
}
.active-button {
background: rgb(226 249 225) !important;
box-sizing: border-box !important;
border: 1px solid rgb(59 153 29 / 84%) !important;
color: rgb(17 193 56 / 93%) !important;
cursor: pointer;
outline: none;
font-family: "Google Sans", sans-serif !important;
font-size: 14px;
font-weight: 500;
margin: 0;
min-width: 64px;
line-height: 36px;
padding: 0px 45px;
height: 60%;
margin-left: 2rem;
border-radius: 48px !important;
display: flex;
justify-content: center;
flex-direction: column;
flex-wrap: wrap;
align-content: center;
align-items: center;
}
.intent-field {
width: 50%;
.my-icon {
margin-right: 0.5rem;
color: #005cbb;
border-radius: 2rem;
padding: 1rem;
}
}
.intent-heading-container {
font-style: normal;
font-weight: 400;
font-size: 16px;
line-height: 23px;
color: #363e45;
padding-top: 2%;
padding-bottom: 1%;
}
.internal-spinner-container {
height: 31px
}
pre {
width: 100%;
color: white;
padding: 2%;
font-family: monospace;
text-wrap: pretty;
display: flex;
flex-direction: row;
}
::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
border-radius: 10px;
background-color: #F5F5F5;
}
::-webkit-scrollbar {
width: 12px;
background-color: #F5F5F5;
}
::-webkit-scrollbar-thumb {
border-radius: 10px;
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, .3);
background-color: #D62929;
}
.intent-chunk {
display: flex;
flex-direction: row;
width: 90%;
}
.copy-button-container {
width: 10%;
display: flex;
justify-content: flex-end;
}
.configure-title {
display: flex;
align-content: center;
flex-wrap: wrap;
width: 50%;
}
.intent-buttons-container {
display: flex;
gap: 2%;
justify-content: flex-end;
padding-top: 1%;
padding-bottom: 1%;
}
.image-container {
justify-content: center;
display: flex;
width: 90%;
img {
height: 47%;
width: 47%;
}
}
.delete-confirmation-text {
font-family: 'Google Sans', sans-serif !important;
font-style: normal;
width: 100%;
display: flex;
justify-content: center;
font-size: 20px;
color: #161A1D;
}
.dialog-actions-container {
padding-bottom: 2%;
justify-content: center;
display: flex;
flex-direction: row;
text-align: end;
width: 100%;
padding-right: 2%;
.discard-button {
color: rgba(92, 95, 97, 0.5);
border: 1px solid rgb(92 95 97 / 28%);
border-radius: 40px;
box-shadow: none;
letter-spacing: inherit;
}
.save-button {
background: #4285F4;
color: #fff;
border-radius: 40px;
box-shadow: none;
letter-spacing: inherit;
}
.dialog-delete-button {
border-radius: 30px;
font-family: 'Google Sans', sans-serif !important;
font-style: normal;
font-weight: 500;
font-size: 14px;
line-height: 23px;
letter-spacing: unset;
box-shadow: none;
color: #f70808;
border: 1px solid #f70808;
}
}
@@ -0,0 +1,38 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {ManageSearchApplicationComponent} from './manage-search-application.component';
describe('ManageAgentBuilderComponent', () => {
let component: ManageSearchApplicationComponent;
let fixture: ComponentFixture<ManageSearchApplicationComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ManageSearchApplicationComponent],
}).compileComponents();
fixture = TestBed.createComponent(ManageSearchApplicationComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,107 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Component, OnDestroy} from '@angular/core';
import {SearchApplicationService} from 'src/app/services/search_application.service';
import {ReplaySubject} from 'rxjs';
import {takeUntil} from 'rxjs/operators';
import {UserService} from 'src/app/services/user/user.service';
import {Router} from '@angular/router';
import {Engine} from 'src/app/models/engine.model';
import {EnginesService} from 'src/app/services/engines.service';
@Component({
selector: 'app-manage-search-application',
templateUrl: './manage-search-application.component.html',
styleUrls: ['./manage-search-application.component.scss'],
})
export class ManageSearchApplicationComponent implements OnDestroy {
selectedEngine: Engine | undefined;
engines: Engine[] = [];
editMode = false;
savedEngineID = '';
private readonly destroyed = new ReplaySubject<void>(1);
constructor(
private readonly searchApplicationService: SearchApplicationService,
private readonly userService: UserService,
private readonly router: Router,
private enginesService: EnginesService
) {
this.enginesService
.getAll()
.subscribe(response => (this.engines = response));
this.disableForm();
this.getConfigData();
}
disableForm() {
this.editMode = false;
}
enableForm() {
this.editMode = true;
}
getConfigData() {
this.userService.showLoading();
this.searchApplicationService
.get()
.pipe(takeUntil(this.destroyed))
.subscribe({
next: response => {
this.savedEngineID = response.engine_id;
this.selectedEngine = this.engines.filter(
e => e.engine_id === response.engine_id
)[0];
this.userService.hideLoading();
},
error: () => {
this.userService.hideLoading();
},
});
}
saveForm() {
this.userService.showLoading();
const searchApplication = {
engine_id: this.selectedEngine?.engine_id || "",
region: this.selectedEngine?.region || "",
};
this.searchApplicationService
.update(this.savedEngineID, searchApplication)
.subscribe({
next: () => {
this.savedEngineID = this.selectedEngine?.engine_id || "";
this.userService.hideLoading();
this.disableForm();
},
error: () => {
this.userService.hideLoading();
},
});
}
navigateToMain() {
this.router.navigateByUrl('/');
}
ngOnDestroy(): void {
this.destroyed.next();
this.destroyed.complete();
}
}
@@ -0,0 +1,52 @@
<!--
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div>
<div class="configure-title">
<span>Configure Search Application</span>
</div>
<form>
<mat-form-field
class="form-field"
appearance="outline"
[floatLabel]="'always'"
>
<mat-label>Choose an Engine</mat-label>
<mat-select
[(ngModel)]="selectedEngine"
name="engine"
>
<mat-option *ngFor="let engine of this.engines" [value]="engine">{{engine.name}}</mat-option>
</mat-select>
</mat-form-field>
</form>
<div class="button-container">
<button
mat-button
*ngIf="!this.showSpinner"
class="submit-button"
[disabled]="!this.selectedEngine"
(click)="saveForm()"
>
Save
</button>
<div class="spinner-container" *ngIf="this.showSpinner">
<mat-spinner style="width: 60px; height: 60px"></mat-spinner>
</div>
</div>
</div>
@@ -0,0 +1,106 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.configure-title {
display: flex;
align-content: center;
flex-wrap: wrap;
width: 100%;
justify-content: center;
margin-top: -26px;
padding-bottom: 5%;
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
font-size: 24px;
line-height: 24px;
color: #161A1D;
}
.form-field {
width: 70%;
.my-icon {
margin-right: 0.5rem;
color: #005cbb;
border-radius: 2rem;
padding: 1rem;
}
}
.button-container {
justify-content: center;
display: flex;
gap: 10px;
margin-bottom: -30px;
padding-top: 3%;
.submit-button {
width: 7%;
background: #4285F4;
box-sizing: border-box;
border: 1px solid #e0e0e0;
color: #fff;
position: relative;
user-select: none;
cursor: pointer;
outline: none;
-webkit-tap-highlight-color: transparent;
display: inline-block;
font-family: 'Google Sans', sans-serif !important;
font-size: 14px;
font-weight: 500;
white-space: nowrap;
text-decoration: none;
vertical-align: baseline;
text-align: center;
margin: 0;
min-width: 64px;
padding: 0 16px;
border-radius: 35px;
overflow: visible;
transform: translate3d(0, 0, 0);
transition: background 400ms cubic-bezier(0.25, 0.8, 0.25, 1), box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1);
}
.cancel-button {
width: 7%;
background: white;
box-sizing: border-box;
border: 1px solid #4285F4;
color: #4285F4;
position: relative;
user-select: none;
cursor: pointer;
outline: none;
-webkit-tap-highlight-color: transparent;
display: inline-block;
font-family: 'Google Sans', sans-serif !important;
font-size: 14px;
font-weight: 500;
white-space: nowrap;
text-decoration: none;
vertical-align: baseline;
text-align: center;
margin: 0;
min-width: 64px;
padding: 0 16px;
border-radius: 35px;
overflow: visible;
transform: translate3d(0, 0, 0);
transition: background 400ms cubic-bezier(0.25, 0.8, 0.25, 1), box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1);
}
}
@@ -0,0 +1,38 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {SearchApplicationFormComponent} from './search-application-form.component';
describe('AddAgentBuilderComponent', () => {
let component: SearchApplicationFormComponent;
let fixture: ComponentFixture<SearchApplicationFormComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [SearchApplicationFormComponent],
}).compileComponents();
fixture = TestBed.createComponent(SearchApplicationFormComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,66 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Component} from '@angular/core';
import {MatDialogRef} from '@angular/material/dialog';
import {Router} from '@angular/router';
import {Engine} from 'src/app/models/engine.model';
import {EnginesService} from 'src/app/services/engines.service';
import {SearchApplicationService} from 'src/app/services/search_application.service';
import {UserService} from 'src/app/services/user/user.service';
@Component({
selector: 'app-search-application-form',
templateUrl: './search-application-form.component.html',
styleUrls: ['./search-application-form.component.scss'],
})
export class SearchApplicationFormComponent {
showSpinner = false;
selectedEngine: Engine | undefined;
engines: Engine[] = [];
constructor(
private dialogRef: MatDialogRef<SearchApplicationFormComponent>,
private readonly router: Router,
private readonly searchApplicationService: SearchApplicationService,
private userService: UserService,
private enginesService: EnginesService
) {
this.enginesService
.getAll()
.subscribe(response => (this.engines = response));
}
saveForm() {
if (this.selectedEngine) {
const searchApplication = {
engine_id: this.selectedEngine.engine_id,
region: this.selectedEngine.region,
};
this.userService.showLoading();
this.searchApplicationService.create(searchApplication).subscribe({
next: () => {
this.userService.hideLoading();
this.dialogRef.close()!;
this.router.navigateByUrl('/');
},
error: () => {
this.userService.hideLoading();
},
});
}
}
}
@@ -0,0 +1,26 @@
<!--
Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div fxLayout="row" fxLayoutGap="2%">
<img src="assets/images/{{ icon }}.svg" alt="icon inside toast message" />
<span class="toast-content">{{ text }}</span>
<img
class="close-toast"
src="assets/images/cancel-toast.svg"
alt="cross icon inside toast message"
(click)="closeToast()"
/>
</div>
@@ -0,0 +1,37 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.toast-content {
color: #ffffff;
font-family: "Google Sans", sans-serif !important;
}
.close-toast {
margin-left: auto;
align-self: baseline;
cursor: pointer;
}
.red-toast {
background-color: #ed0c0c !important;
}
.green-toast {
background-color: #06865e !important;
}
.mat-mdc-snack-bar-container .mdc-snackbar__surface {
background-color: transparent !important;
}
@@ -0,0 +1,38 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {ToastMessageComponent} from './toast-message.component';
describe('ToastMessageComponent', () => {
let component: ToastMessageComponent;
let fixture: ComponentFixture<ToastMessageComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ToastMessageComponent],
}).compileComponents();
fixture = TestBed.createComponent(ToastMessageComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,40 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Component, Inject, ViewEncapsulation} from '@angular/core';
import {MatSnackBar, MAT_SNACK_BAR_DATA} from '@angular/material/snack-bar';
@Component({
selector: 'app-toast-message',
templateUrl: './toast-message.component.html',
styleUrls: ['./toast-message.component.scss'],
encapsulation: ViewEncapsulation.None,
})
export class ToastMessageComponent {
text: string;
icon: string;
constructor(
private _snackBar: MatSnackBar,
@Inject(MAT_SNACK_BAR_DATA) public snackBarData: any
) {
this.text = snackBarData.text;
this.icon = snackBarData.icon;
}
closeToast() {
this._snackBar.dismiss();
}
}
@@ -0,0 +1,20 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export type config = {
name: string;
url: string;
};
@@ -0,0 +1,21 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export type Engine = {
name: string;
engine_id: string;
region: string;
};
@@ -0,0 +1,52 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export type SearchRequest = {
term: string;
};
export type SearchResponse = {
summary: any;
results: SearchResult[];
totalSize: number;
};
export type SearchResult = {
document: Document;
};
export type Document = {
derivedStructData: DocumentData;
};
export type DocumentData = {
title: string;
link: string;
snippets: Snippet[];
pagemap: PageMap;
};
export type Snippet = {
snippet: string;
};
export type PageMap = {
cse_image: ImagesData[];
};
export type ImagesData = {
src: string;
};
@@ -0,0 +1,20 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export type SearchApplication = {
engine_id: string;
region: string;
};
@@ -0,0 +1,24 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {TruncatePipe} from './truncate.pipe';
describe('TruncatePipe', () => {
it('create an instance', () => {
const pipe = new TruncatePipe();
expect(pipe).toBeTruthy();
});
});
@@ -0,0 +1,26 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Pipe, PipeTransform} from '@angular/core';
@Pipe({
name: 'truncate',
})
export class TruncatePipe implements PipeTransform {
transform(value: string, limit = 300, trail = '...'): string {
return value.length > limit ? value.substring(0, limit) + trail : value;
}
}
@@ -0,0 +1,32 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {TestBed} from '@angular/core/testing';
import {EnginesService} from './engines.service';
describe('EnginesService', () => {
let service: EnginesService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(EnginesService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});
@@ -0,0 +1,36 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {map, Observable} from 'rxjs';
import {HttpClient} from '@angular/common/http';
import {Injectable} from '@angular/core';
import {Engine} from '../models/engine.model';
import {environment} from 'src/environments/environment';
const ENGINES_URL = `${environment.backendURL}/search/engines`;
@Injectable({
providedIn: 'root',
})
export class EnginesService {
constructor(private http: HttpClient) {}
getAll(): Observable<Engine[]> {
return this.http
.get(ENGINES_URL)
.pipe(map(response => response as Engine[]));
}
}
@@ -0,0 +1,32 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {TestBed} from '@angular/core/testing';
import {AuthGuard} from './auth.guard';
describe('AuthGuard', () => {
let guard: AuthGuard;
beforeEach(() => {
TestBed.configureTestingModule({});
guard = TestBed.inject(AuthGuard);
});
it('should be created', () => {
expect(guard).toBeTruthy();
});
});
@@ -0,0 +1,54 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Injectable} from '@angular/core';
import {
ActivatedRouteSnapshot,
CanActivate,
Router,
RouterStateSnapshot,
UrlTree,
} from '@angular/router';
import {Observable} from 'rxjs';
import {AuthService} from './auth.service';
import {environment} from 'src/environments/environment';
const LOGIN_ROUTE = '/login';
@Injectable({
providedIn: 'root',
})
export class AuthGuard implements CanActivate {
constructor(
private auth: AuthService,
private router: Router
) {}
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
):
| Observable<boolean | UrlTree>
| Promise<boolean | UrlTree>
| boolean
| UrlTree {
if (environment.requiredLogin === 'True' && !this.auth.isLoggedIn()) {
this.router.navigate([LOGIN_ROUTE]);
return false;
}
return true;
}
}
@@ -0,0 +1,32 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {TestBed} from '@angular/core/testing';
import {AuthService} from './auth.service';
describe('AuthService', () => {
let service: AuthService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(AuthService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});
@@ -0,0 +1,57 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Injectable} from '@angular/core';
import {Router} from '@angular/router';
import {environment} from 'src/environments/environment';
const USER_TOKEN_KEY = 'gpau_id';
const USER_DETAILS = 'USER_DETAILS';
const LOGIN_ROUTE = '/login';
@Injectable({
providedIn: 'root',
})
export class AuthService {
private requiredLogin: boolean = environment.requiredLogin === 'True';
constructor(private router: Router) {}
saveUserSession(token: string) {
localStorage.setItem(USER_TOKEN_KEY, token);
}
logout(route: string = LOGIN_ROUTE) {
localStorage.removeItem(USER_TOKEN_KEY);
localStorage.removeItem(USER_DETAILS);
localStorage.removeItem('showTooltip');
this.router.navigateByUrl(route);
}
isLoggedIn() {
const isLoggedIn = localStorage.getItem(USER_TOKEN_KEY) !== null;
if (!isLoggedIn && this.router.url !== LOGIN_ROUTE) {
this.router.navigate([LOGIN_ROUTE]);
}
return isLoggedIn;
}
isUserLoggedIn() {
if (!this.requiredLogin) return true;
const isLoggedIn = localStorage.getItem(USER_TOKEN_KEY) !== null;
return isLoggedIn;
}
}
@@ -0,0 +1,32 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {TestBed} from '@angular/core/testing';
import {SearchService} from './search.service';
describe('SearchService', () => {
let service: SearchService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(SearchService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});
@@ -0,0 +1,37 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {HttpClient} from '@angular/common/http';
import {Injectable} from '@angular/core';
import {environment} from 'src/environments/environment';
import {map} from 'rxjs/operators';
import {SearchRequest, SearchResponse} from '../models/search.model';
const searchURL = `${environment.backendURL}/search`;
@Injectable({
providedIn: 'root',
})
export class SearchService {
constructor(private http: HttpClient) {}
search(term: string) {
const request: SearchRequest = {term: term};
return this.http
.post(searchURL, request)
.pipe(map(response => response as SearchResponse));
}
}
@@ -0,0 +1,32 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {TestBed} from '@angular/core/testing';
import {SearchApplicationService} from './search_application.service';
describe('SearchApplicationService', () => {
let service: SearchApplicationService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(SearchApplicationService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});
@@ -0,0 +1,45 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Injectable} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {environment} from 'src/environments/environment';
import {map} from 'rxjs/operators';
import {Observable} from 'rxjs';
import {SearchApplication} from '../models/search_application.model';
const configURL = `${environment.backendURL}/search/application`;
@Injectable({
providedIn: 'root',
})
export class SearchApplicationService {
constructor(private readonly http: HttpClient) {}
get(): Observable<SearchApplication> {
return this.http
.get(configURL)
.pipe(map(response => response as SearchApplication));
}
create(searchApplication: SearchApplication) {
return this.http.post(configURL, searchApplication);
}
update(engine_id: string, searchApplication: SearchApplication) {
return this.http.put(`${configURL}/${engine_id}`, searchApplication);
}
}

Some files were not shown because too many files have changed in this diff Show More