init
36
README.md
Normal file
|
@ -0,0 +1,36 @@
|
|||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
12
backend/.gitignore
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
__pycache__
|
||||
__pycache__/
|
||||
.__pycache__
|
830
backend/LLMGraphTransformer.py
Normal file
|
@ -0,0 +1,830 @@
|
|||
import asyncio
|
||||
import json
|
||||
from typing import Any, Dict, List, Optional, Sequence, Tuple, Type, Union, cast
|
||||
|
||||
from langchain_community.graphs.graph_document import GraphDocument, Node, Relationship
|
||||
from langchain_core.documents import Document
|
||||
from langchain_core.language_models import BaseLanguageModel
|
||||
from langchain_core.messages import SystemMessage
|
||||
from langchain_core.output_parsers import JsonOutputParser
|
||||
from langchain_core.prompts import (
|
||||
ChatPromptTemplate,
|
||||
HumanMessagePromptTemplate,
|
||||
PromptTemplate,
|
||||
)
|
||||
from langchain_core.pydantic_v1 import BaseModel, Field, create_model
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
|
||||
examples = [
|
||||
{
|
||||
"text": (
|
||||
"Adam is a software engineer in Microsoft since 2009, "
|
||||
"and last year he got an award as the Best Talent"
|
||||
),
|
||||
"head": "Adam",
|
||||
"head_type": "Person",
|
||||
"relation": "WORKS_FOR",
|
||||
"tail": "Microsoft",
|
||||
"tail_type": "Company",
|
||||
},
|
||||
{
|
||||
"text": (
|
||||
"Adam is a software engineer in Microsoft since 2009, "
|
||||
"and last year he got an award as the Best Talent"
|
||||
),
|
||||
"head": "Adam",
|
||||
"head_type": "Person",
|
||||
"relation": "HAS_AWARD",
|
||||
"tail": "Best Talent",
|
||||
"tail_type": "Award",
|
||||
},
|
||||
{
|
||||
"text": (
|
||||
"Microsoft is a tech company that provide "
|
||||
"several products such as Microsoft Word"
|
||||
),
|
||||
"head": "Microsoft Word",
|
||||
"head_type": "Product",
|
||||
"relation": "PRODUCED_BY",
|
||||
"tail": "Microsoft",
|
||||
"tail_type": "Company",
|
||||
},
|
||||
{
|
||||
"text": "Microsoft Word is a lightweight app that accessible offline",
|
||||
"head": "Microsoft Word",
|
||||
"head_type": "Product",
|
||||
"relation": "HAS_CHARACTERISTIC",
|
||||
"tail": "lightweight app",
|
||||
"tail_type": "Characteristic",
|
||||
},
|
||||
{
|
||||
"text": "Microsoft Word is a lightweight app that accessible offline",
|
||||
"head": "Microsoft Word",
|
||||
"head_type": "Product",
|
||||
"relation": "HAS_CHARACTERISTIC",
|
||||
"tail": "accessible offline",
|
||||
"tail_type": "Characteristic",
|
||||
},
|
||||
]
|
||||
|
||||
system_prompt = (
|
||||
"# Knowledge Graph Instructions for GPT-4\n"
|
||||
"## 1. Overview\n"
|
||||
"You are a top-tier algorithm designed for extracting information in structured "
|
||||
"formats to build a knowledge graph.\n"
|
||||
"Try to capture as much information from the text as possible without "
|
||||
"sacrificing accuracy. Do not add any information that is not explicitly "
|
||||
"mentioned in the text.\n"
|
||||
"- **Nodes** represent entities and concepts.\n"
|
||||
"- The aim is to achieve simplicity and clarity in the knowledge graph, making it\n"
|
||||
"accessible for a vast audience.\n"
|
||||
"## 2. Labeling Nodes\n"
|
||||
"- **Consistency**: Ensure you use available types for node labels.\n"
|
||||
"Ensure you use basic or elementary types for node labels.\n"
|
||||
"- For example, when you identify an entity representing a person, "
|
||||
"always label it as **'person'**. Avoid using more specific terms "
|
||||
"like 'mathematician' or 'scientist'."
|
||||
"- **Node IDs**: Never utilize integers as node IDs. Node IDs should be "
|
||||
"names or human-readable identifiers found in the text.\n"
|
||||
"- **Relationships** represent connections between entities or concepts.\n"
|
||||
"Ensure consistency and generality in relationship types when constructing "
|
||||
"knowledge graphs. Instead of using specific and momentary types "
|
||||
"such as 'BECAME_PROFESSOR', use more general and timeless relationship types "
|
||||
"like 'PROFESSOR'. Make sure to use general and timeless relationship types!\n"
|
||||
"## 3. Coreference Resolution\n"
|
||||
"- **Maintain Entity Consistency**: When extracting entities, it's vital to "
|
||||
"ensure consistency.\n"
|
||||
'If an entity, such as "John Doe", is mentioned multiple times in the text '
|
||||
'but is referred to by different names or pronouns (e.g., "Joe", "he"),'
|
||||
"always use the most complete identifier for that entity throughout the "
|
||||
'knowledge graph. In this example, use "John Doe" as the entity ID.\n'
|
||||
"Remember, the knowledge graph should be coherent and easily understandable, "
|
||||
"so maintaining consistency in entity references is crucial.\n"
|
||||
"## 4. Strict Compliance\n"
|
||||
"Adhere to the rules strictly. Non-compliance will result in termination."
|
||||
)
|
||||
|
||||
default_prompt = ChatPromptTemplate.from_messages(
|
||||
[
|
||||
(
|
||||
"system",
|
||||
system_prompt,
|
||||
),
|
||||
(
|
||||
"human",
|
||||
(
|
||||
"Note: The information given to you is information about a User's Web Browsing History."
|
||||
"Tip: Make sure to answer in the correct format and do "
|
||||
"not include any explanations. "
|
||||
"Use the given format to extract information from the "
|
||||
"following input: {input}"
|
||||
),
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def _get_additional_info(input_type: str) -> str:
|
||||
# Check if the input_type is one of the allowed values
|
||||
if input_type not in ["node", "relationship", "property"]:
|
||||
raise ValueError("input_type must be 'node', 'relationship', or 'property'")
|
||||
|
||||
# Perform actions based on the input_type
|
||||
if input_type == "node":
|
||||
return (
|
||||
"Ensure you use basic or elementary types for node labels.\n"
|
||||
"For example, when you identify an entity representing a person, "
|
||||
"always label it as **'Person'**. Avoid using more specific terms "
|
||||
"like 'Mathematician' or 'Scientist'"
|
||||
)
|
||||
elif input_type == "relationship":
|
||||
return (
|
||||
"Instead of using specific and momentary types such as "
|
||||
"'BECAME_PROFESSOR', use more general and timeless relationship types "
|
||||
"like 'PROFESSOR'. However, do not sacrifice any accuracy for generality"
|
||||
)
|
||||
elif input_type == "property":
|
||||
return ""
|
||||
return ""
|
||||
|
||||
|
||||
def optional_enum_field(
|
||||
enum_values: Optional[List[str]] = None,
|
||||
description: str = "",
|
||||
input_type: str = "node",
|
||||
llm_type: Optional[str] = None,
|
||||
**field_kwargs: Any,
|
||||
) -> Any:
|
||||
"""Utility function to conditionally create a field with an enum constraint."""
|
||||
# Only openai supports enum param
|
||||
if enum_values and llm_type == "openai-chat":
|
||||
return Field(
|
||||
...,
|
||||
enum=enum_values,
|
||||
description=f"{description}. Available options are {enum_values}",
|
||||
**field_kwargs,
|
||||
)
|
||||
elif enum_values:
|
||||
return Field(
|
||||
...,
|
||||
description=f"{description}. Available options are {enum_values}",
|
||||
**field_kwargs,
|
||||
)
|
||||
else:
|
||||
additional_info = _get_additional_info(input_type)
|
||||
return Field(..., description=description + additional_info, **field_kwargs)
|
||||
|
||||
|
||||
class _Graph(BaseModel):
|
||||
nodes: Optional[List]
|
||||
relationships: Optional[List]
|
||||
|
||||
|
||||
class UnstructuredRelation(BaseModel):
|
||||
head: str = Field(
|
||||
description=(
|
||||
"extracted head entity like Microsoft, Apple, John. "
|
||||
"Must use human-readable unique identifier."
|
||||
)
|
||||
)
|
||||
head_type: str = Field(
|
||||
description="type of the extracted head entity like Person, Company, etc"
|
||||
)
|
||||
relation: str = Field(description="relation between the head and the tail entities")
|
||||
tail: str = Field(
|
||||
description=(
|
||||
"extracted tail entity like Microsoft, Apple, John. "
|
||||
"Must use human-readable unique identifier."
|
||||
)
|
||||
)
|
||||
tail_type: str = Field(
|
||||
description="type of the extracted tail entity like Person, Company, etc"
|
||||
)
|
||||
|
||||
|
||||
def create_unstructured_prompt(
|
||||
node_labels: Optional[List[str]] = None, rel_types: Optional[List[str]] = None
|
||||
) -> ChatPromptTemplate:
|
||||
node_labels_str = str(node_labels) if node_labels else ""
|
||||
rel_types_str = str(rel_types) if rel_types else ""
|
||||
base_string_parts = [
|
||||
"You are a top-tier algorithm designed for extracting information in "
|
||||
"structured formats to build a knowledge graph. Your task is to identify "
|
||||
"the entities and relations requested with the user prompt from a given "
|
||||
"text. You must generate the output in a JSON format containing a list "
|
||||
'with JSON objects. Each object should have the keys: "head", '
|
||||
'"head_type", "relation", "tail", and "tail_type". The "head" '
|
||||
"key must contain the text of the extracted entity with one of the types "
|
||||
"from the provided list in the user prompt.",
|
||||
f'The "head_type" key must contain the type of the extracted head entity, '
|
||||
f"which must be one of the types from {node_labels_str}."
|
||||
if node_labels
|
||||
else "",
|
||||
f'The "relation" key must contain the type of relation between the "head" '
|
||||
f'and the "tail", which must be one of the relations from {rel_types_str}.'
|
||||
if rel_types
|
||||
else "",
|
||||
f'The "tail" key must represent the text of an extracted entity which is '
|
||||
f'the tail of the relation, and the "tail_type" key must contain the type '
|
||||
f"of the tail entity from {node_labels_str}."
|
||||
if node_labels
|
||||
else "",
|
||||
"Attempt to extract as many entities and relations as you can. Maintain "
|
||||
"Entity Consistency: When extracting entities, it's vital to ensure "
|
||||
'consistency. If an entity, such as "John Doe", is mentioned multiple '
|
||||
"times in the text but is referred to by different names or pronouns "
|
||||
'(e.g., "Joe", "he"), always use the most complete identifier for '
|
||||
"that entity. The knowledge graph should be coherent and easily "
|
||||
"understandable, so maintaining consistency in entity references is "
|
||||
"crucial.",
|
||||
"IMPORTANT NOTES:\n- Don't add any explanation and text.",
|
||||
]
|
||||
system_prompt = "\n".join(filter(None, base_string_parts))
|
||||
|
||||
system_message = SystemMessage(content=system_prompt)
|
||||
parser = JsonOutputParser(pydantic_object=UnstructuredRelation)
|
||||
|
||||
human_string_parts = [
|
||||
"Based on the following example, extract entities and "
|
||||
"relations from the provided text.\n\n",
|
||||
"Use the following entity types, don't use other entity "
|
||||
"that is not defined below:"
|
||||
"# ENTITY TYPES:"
|
||||
"{node_labels}"
|
||||
if node_labels
|
||||
else "",
|
||||
"Use the following relation types, don't use other relation "
|
||||
"that is not defined below:"
|
||||
"# RELATION TYPES:"
|
||||
"{rel_types}"
|
||||
if rel_types
|
||||
else "",
|
||||
"Below are a number of examples of text and their extracted "
|
||||
"entities and relationships."
|
||||
"{examples}\n"
|
||||
"For the following text, extract entities and relations as "
|
||||
"in the provided example."
|
||||
"{format_instructions}\nText: {input}",
|
||||
]
|
||||
human_prompt_string = "\n".join(filter(None, human_string_parts))
|
||||
human_prompt = PromptTemplate(
|
||||
template=human_prompt_string,
|
||||
input_variables=["input"],
|
||||
partial_variables={
|
||||
"format_instructions": parser.get_format_instructions(),
|
||||
"node_labels": node_labels,
|
||||
"rel_types": rel_types,
|
||||
"examples": examples,
|
||||
},
|
||||
)
|
||||
|
||||
human_message_prompt = HumanMessagePromptTemplate(prompt=human_prompt)
|
||||
|
||||
chat_prompt = ChatPromptTemplate.from_messages(
|
||||
[system_message, human_message_prompt]
|
||||
)
|
||||
return chat_prompt
|
||||
|
||||
|
||||
def create_simple_model(
|
||||
node_labels: Optional[List[str]] = None,
|
||||
rel_types: Optional[List[str]] = None,
|
||||
node_properties: Union[bool, List[str]] = False,
|
||||
llm_type: Optional[str] = None,
|
||||
relationship_properties: Union[bool, List[str]] = False,
|
||||
) -> Type[_Graph]:
|
||||
"""
|
||||
Create a simple graph model with optional constraints on node
|
||||
and relationship types.
|
||||
|
||||
Args:
|
||||
node_labels (Optional[List[str]]): Specifies the allowed node types.
|
||||
Defaults to None, allowing all node types.
|
||||
rel_types (Optional[List[str]]): Specifies the allowed relationship types.
|
||||
Defaults to None, allowing all relationship types.
|
||||
node_properties (Union[bool, List[str]]): Specifies if node properties should
|
||||
be included. If a list is provided, only properties with keys in the list
|
||||
will be included. If True, all properties are included. Defaults to False.
|
||||
relationship_properties (Union[bool, List[str]]): Specifies if relationship
|
||||
properties should be included. If a list is provided, only properties with
|
||||
keys in the list will be included. If True, all properties are included.
|
||||
Defaults to False.
|
||||
llm_type (Optional[str]): The type of the language model. Defaults to None.
|
||||
Only openai supports enum param: openai-chat.
|
||||
|
||||
Returns:
|
||||
Type[_Graph]: A graph model with the specified constraints.
|
||||
|
||||
Raises:
|
||||
ValueError: If 'id' is included in the node or relationship properties list.
|
||||
"""
|
||||
|
||||
node_fields: Dict[str, Tuple[Any, Any]] = {
|
||||
"id": (
|
||||
str,
|
||||
Field(..., description="Name or human-readable unique identifier."),
|
||||
),
|
||||
"type": (
|
||||
str,
|
||||
optional_enum_field(
|
||||
node_labels,
|
||||
description="The type or label of the node.",
|
||||
input_type="node",
|
||||
llm_type=llm_type,
|
||||
),
|
||||
),
|
||||
}
|
||||
|
||||
if node_properties:
|
||||
if isinstance(node_properties, list) and "id" in node_properties:
|
||||
raise ValueError("The node property 'id' is reserved and cannot be used.")
|
||||
# Map True to empty array
|
||||
node_properties_mapped: List[str] = (
|
||||
[] if node_properties is True else node_properties
|
||||
)
|
||||
|
||||
class Property(BaseModel):
|
||||
"""A single property consisting of key and value"""
|
||||
|
||||
key: str = optional_enum_field(
|
||||
node_properties_mapped,
|
||||
description="Property key.",
|
||||
input_type="property",
|
||||
llm_type=llm_type,
|
||||
)
|
||||
value: str = Field(..., description="value")
|
||||
|
||||
node_fields["properties"] = (
|
||||
Optional[List[Property]],
|
||||
Field(None, description="List of node properties"),
|
||||
)
|
||||
SimpleNode = create_model("SimpleNode", **node_fields) # type: ignore
|
||||
|
||||
relationship_fields: Dict[str, Tuple[Any, Any]] = {
|
||||
"source_node_id": (
|
||||
str,
|
||||
Field(
|
||||
...,
|
||||
description="Name or human-readable unique identifier of source node",
|
||||
),
|
||||
),
|
||||
"source_node_type": (
|
||||
str,
|
||||
optional_enum_field(
|
||||
node_labels,
|
||||
description="The type or label of the source node.",
|
||||
input_type="node",
|
||||
llm_type=llm_type,
|
||||
),
|
||||
),
|
||||
"target_node_id": (
|
||||
str,
|
||||
Field(
|
||||
...,
|
||||
description="Name or human-readable unique identifier of target node",
|
||||
),
|
||||
),
|
||||
"target_node_type": (
|
||||
str,
|
||||
optional_enum_field(
|
||||
node_labels,
|
||||
description="The type or label of the target node.",
|
||||
input_type="node",
|
||||
llm_type=llm_type,
|
||||
),
|
||||
),
|
||||
"type": (
|
||||
str,
|
||||
optional_enum_field(
|
||||
rel_types,
|
||||
description="The type of the relationship.",
|
||||
input_type="relationship",
|
||||
llm_type=llm_type,
|
||||
),
|
||||
),
|
||||
}
|
||||
if relationship_properties:
|
||||
if (
|
||||
isinstance(relationship_properties, list)
|
||||
and "id" in relationship_properties
|
||||
):
|
||||
raise ValueError(
|
||||
"The relationship property 'id' is reserved and cannot be used."
|
||||
)
|
||||
# Map True to empty array
|
||||
relationship_properties_mapped: List[str] = (
|
||||
[] if relationship_properties is True else relationship_properties
|
||||
)
|
||||
|
||||
class RelationshipProperty(BaseModel):
|
||||
"""A single property consisting of key and value"""
|
||||
|
||||
key: str = optional_enum_field(
|
||||
relationship_properties_mapped,
|
||||
description="Property key.",
|
||||
input_type="property",
|
||||
llm_type=llm_type,
|
||||
)
|
||||
value: str = Field(..., description="value")
|
||||
|
||||
relationship_fields["properties"] = (
|
||||
Optional[List[RelationshipProperty]],
|
||||
Field(None, description="List of relationship properties"),
|
||||
)
|
||||
SimpleRelationship = create_model("SimpleRelationship", **relationship_fields) # type: ignore
|
||||
|
||||
class DynamicGraph(_Graph):
|
||||
"""Represents a graph document consisting of nodes and relationships."""
|
||||
|
||||
nodes: Optional[List[SimpleNode]] = Field(description="List of nodes") # type: ignore
|
||||
relationships: Optional[List[SimpleRelationship]] = Field( # type: ignore
|
||||
description="List of relationships"
|
||||
)
|
||||
|
||||
return DynamicGraph
|
||||
|
||||
|
||||
def map_to_base_node(node: Any) -> Node:
|
||||
"""Map the SimpleNode to the base Node."""
|
||||
properties = {}
|
||||
if hasattr(node, "properties") and node.properties:
|
||||
for p in node.properties:
|
||||
properties[format_property_key(p.key)] = p.value
|
||||
return Node(id=node.id, type=node.type, properties=properties)
|
||||
|
||||
|
||||
def map_to_base_relationship(rel: Any) -> Relationship:
|
||||
"""Map the SimpleRelationship to the base Relationship."""
|
||||
source = Node(id=rel.source_node_id, type=rel.source_node_type)
|
||||
target = Node(id=rel.target_node_id, type=rel.target_node_type)
|
||||
properties = {}
|
||||
if hasattr(rel, "properties") and rel.properties:
|
||||
for p in rel.properties:
|
||||
properties[format_property_key(p.key)] = p.value
|
||||
return Relationship(
|
||||
source=source, target=target, type=rel.type, properties=properties
|
||||
)
|
||||
|
||||
|
||||
def _parse_and_clean_json(
|
||||
argument_json: Dict[str, Any],
|
||||
) -> Tuple[List[Node], List[Relationship]]:
|
||||
nodes = []
|
||||
for node in argument_json["nodes"]:
|
||||
if not node.get("id"): # Id is mandatory, skip this node
|
||||
continue
|
||||
node_properties = {}
|
||||
if "properties" in node and node["properties"]:
|
||||
for p in node["properties"]:
|
||||
node_properties[format_property_key(p["key"])] = p["value"]
|
||||
nodes.append(
|
||||
Node(
|
||||
id=node["id"],
|
||||
type=node.get("type"),
|
||||
properties=node_properties,
|
||||
)
|
||||
)
|
||||
relationships = []
|
||||
for rel in argument_json["relationships"]:
|
||||
# Mandatory props
|
||||
if (
|
||||
not rel.get("source_node_id")
|
||||
or not rel.get("target_node_id")
|
||||
or not rel.get("type")
|
||||
):
|
||||
continue
|
||||
|
||||
# Node type copying if needed from node list
|
||||
if not rel.get("source_node_type"):
|
||||
try:
|
||||
rel["source_node_type"] = [
|
||||
el.get("type")
|
||||
for el in argument_json["nodes"]
|
||||
if el["id"] == rel["source_node_id"]
|
||||
][0]
|
||||
except IndexError:
|
||||
rel["source_node_type"] = None
|
||||
if not rel.get("target_node_type"):
|
||||
try:
|
||||
rel["target_node_type"] = [
|
||||
el.get("type")
|
||||
for el in argument_json["nodes"]
|
||||
if el["id"] == rel["target_node_id"]
|
||||
][0]
|
||||
except IndexError:
|
||||
rel["target_node_type"] = None
|
||||
|
||||
rel_properties = {}
|
||||
if "properties" in rel and rel["properties"]:
|
||||
for p in rel["properties"]:
|
||||
rel_properties[format_property_key(p["key"])] = p["value"]
|
||||
|
||||
source_node = Node(
|
||||
id=rel["source_node_id"],
|
||||
type=rel["source_node_type"],
|
||||
)
|
||||
target_node = Node(
|
||||
id=rel["target_node_id"],
|
||||
type=rel["target_node_type"],
|
||||
)
|
||||
relationships.append(
|
||||
Relationship(
|
||||
source=source_node,
|
||||
target=target_node,
|
||||
type=rel["type"],
|
||||
properties=rel_properties,
|
||||
)
|
||||
)
|
||||
return nodes, relationships
|
||||
|
||||
|
||||
def _format_nodes(nodes: List[Node]) -> List[Node]:
|
||||
return [
|
||||
Node(
|
||||
id=el.id.title() if isinstance(el.id, str) else el.id,
|
||||
type=el.type.capitalize() # type: ignore[arg-type]
|
||||
if el.type
|
||||
else None, # handle empty strings # type: ignore[arg-type]
|
||||
properties=el.properties,
|
||||
)
|
||||
for el in nodes
|
||||
]
|
||||
|
||||
|
||||
def _format_relationships(rels: List[Relationship]) -> List[Relationship]:
|
||||
return [
|
||||
Relationship(
|
||||
source=_format_nodes([el.source])[0],
|
||||
target=_format_nodes([el.target])[0],
|
||||
type=el.type.replace(" ", "_").upper(),
|
||||
properties=el.properties,
|
||||
)
|
||||
for el in rels
|
||||
]
|
||||
|
||||
|
||||
def format_property_key(s: str) -> str:
|
||||
words = s.split()
|
||||
if not words:
|
||||
return s
|
||||
first_word = words[0].lower()
|
||||
capitalized_words = [word.capitalize() for word in words[1:]]
|
||||
return "".join([first_word] + capitalized_words)
|
||||
|
||||
|
||||
def _convert_to_graph_document(
|
||||
raw_schema: Dict[Any, Any],
|
||||
) -> Tuple[List[Node], List[Relationship]]:
|
||||
# If there are validation errors
|
||||
if not raw_schema["parsed"]:
|
||||
try:
|
||||
try: # OpenAI type response
|
||||
argument_json = json.loads(
|
||||
raw_schema["raw"].additional_kwargs["tool_calls"][0]["function"][
|
||||
"arguments"
|
||||
]
|
||||
)
|
||||
except Exception: # Google type response
|
||||
argument_json = json.loads(
|
||||
raw_schema["raw"].additional_kwargs["function_call"]["arguments"]
|
||||
)
|
||||
|
||||
nodes, relationships = _parse_and_clean_json(argument_json)
|
||||
except Exception: # If we can't parse JSON
|
||||
return ([], [])
|
||||
else: # If there are no validation errors use parsed pydantic object
|
||||
parsed_schema: _Graph = raw_schema["parsed"]
|
||||
nodes = (
|
||||
[map_to_base_node(node) for node in parsed_schema.nodes if node.id]
|
||||
if parsed_schema.nodes
|
||||
else []
|
||||
)
|
||||
|
||||
relationships = (
|
||||
[
|
||||
map_to_base_relationship(rel)
|
||||
for rel in parsed_schema.relationships
|
||||
if rel.type and rel.source_node_id and rel.target_node_id
|
||||
]
|
||||
if parsed_schema.relationships
|
||||
else []
|
||||
)
|
||||
# Title / Capitalize
|
||||
return _format_nodes(nodes), _format_relationships(relationships)
|
||||
|
||||
|
||||
class LLMGraphTransformer:
|
||||
"""Transform documents into graph-based documents using a LLM.
|
||||
|
||||
It allows specifying constraints on the types of nodes and relationships to include
|
||||
in the output graph. The class supports extracting properties for both nodes and
|
||||
relationships.
|
||||
|
||||
Args:
|
||||
llm (BaseLanguageModel): An instance of a language model supporting structured
|
||||
output.
|
||||
allowed_nodes (List[str], optional): Specifies which node types are
|
||||
allowed in the graph. Defaults to an empty list, allowing all node types.
|
||||
allowed_relationships (List[str], optional): Specifies which relationship types
|
||||
are allowed in the graph. Defaults to an empty list, allowing all relationship
|
||||
types.
|
||||
prompt (Optional[ChatPromptTemplate], optional): The prompt to pass to
|
||||
the LLM with additional instructions.
|
||||
strict_mode (bool, optional): Determines whether the transformer should apply
|
||||
filtering to strictly adhere to `allowed_nodes` and `allowed_relationships`.
|
||||
Defaults to True.
|
||||
node_properties (Union[bool, List[str]]): If True, the LLM can extract any
|
||||
node properties from text. Alternatively, a list of valid properties can
|
||||
be provided for the LLM to extract, restricting extraction to those specified.
|
||||
relationship_properties (Union[bool, List[str]]): If True, the LLM can extract
|
||||
any relationship properties from text. Alternatively, a list of valid
|
||||
properties can be provided for the LLM to extract, restricting extraction to
|
||||
those specified.
|
||||
|
||||
Example:
|
||||
.. code-block:: python
|
||||
from langchain_experimental.graph_transformers import LLMGraphTransformer
|
||||
from langchain_core.documents import Document
|
||||
from langchain_openai import ChatOpenAI
|
||||
|
||||
llm=ChatOpenAI(temperature=0)
|
||||
transformer = LLMGraphTransformer(
|
||||
llm=llm,
|
||||
allowed_nodes=["Person", "Organization"])
|
||||
|
||||
doc = Document(page_content="Elon Musk is suing OpenAI")
|
||||
graph_documents = transformer.convert_to_graph_documents([doc])
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
llm: BaseLanguageModel,
|
||||
allowed_nodes: List[str] = [],
|
||||
allowed_relationships: List[str] = [],
|
||||
prompt: Optional[ChatPromptTemplate] = None,
|
||||
strict_mode: bool = True,
|
||||
node_properties: Union[bool, List[str]] = False,
|
||||
relationship_properties: Union[bool, List[str]] = False,
|
||||
) -> None:
|
||||
self.allowed_nodes = allowed_nodes
|
||||
self.allowed_relationships = allowed_relationships
|
||||
self.strict_mode = strict_mode
|
||||
self._function_call = True
|
||||
# Check if the LLM really supports structured output
|
||||
try:
|
||||
llm.with_structured_output(_Graph)
|
||||
except NotImplementedError:
|
||||
self._function_call = False
|
||||
if not self._function_call:
|
||||
if node_properties or relationship_properties:
|
||||
raise ValueError(
|
||||
"The 'node_properties' and 'relationship_properties' parameters "
|
||||
"cannot be used in combination with a LLM that doesn't support "
|
||||
"native function calling."
|
||||
)
|
||||
try:
|
||||
import json_repair # type: ignore
|
||||
|
||||
self.json_repair = json_repair
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Could not import json_repair python package. "
|
||||
"Please install it with `pip install json-repair`."
|
||||
)
|
||||
prompt = prompt or create_unstructured_prompt(
|
||||
allowed_nodes, allowed_relationships
|
||||
)
|
||||
self.chain = prompt | llm
|
||||
else:
|
||||
# Define chain
|
||||
try:
|
||||
llm_type = llm._llm_type # type: ignore
|
||||
except AttributeError:
|
||||
llm_type = None
|
||||
schema = create_simple_model(
|
||||
allowed_nodes,
|
||||
allowed_relationships,
|
||||
node_properties,
|
||||
llm_type,
|
||||
relationship_properties,
|
||||
)
|
||||
structured_llm = llm.with_structured_output(schema, include_raw=True)
|
||||
prompt = prompt or default_prompt
|
||||
self.chain = prompt | structured_llm
|
||||
|
||||
def process_response(
|
||||
self, document: Document, config: Optional[RunnableConfig] = None
|
||||
) -> GraphDocument:
|
||||
"""
|
||||
Processes a single document, transforming it into a graph document using
|
||||
an LLM based on the model's schema and constraints.
|
||||
"""
|
||||
text = document.page_content
|
||||
raw_schema = self.chain.invoke({"input": text}, config=config)
|
||||
if self._function_call:
|
||||
raw_schema = cast(Dict[Any, Any], raw_schema)
|
||||
nodes, relationships = _convert_to_graph_document(raw_schema)
|
||||
else:
|
||||
nodes_set = set()
|
||||
relationships = []
|
||||
if not isinstance(raw_schema, str):
|
||||
raw_schema = raw_schema.content
|
||||
parsed_json = self.json_repair.loads(raw_schema)
|
||||
for rel in parsed_json:
|
||||
# Nodes need to be deduplicated using a set
|
||||
nodes_set.add((rel["head"], rel["head_type"]))
|
||||
nodes_set.add((rel["tail"], rel["tail_type"]))
|
||||
|
||||
source_node = Node(id=rel["head"], type=rel["head_type"])
|
||||
target_node = Node(id=rel["tail"], type=rel["tail_type"])
|
||||
relationships.append(
|
||||
Relationship(
|
||||
source=source_node, target=target_node, type=rel["relation"]
|
||||
)
|
||||
)
|
||||
# Create nodes list
|
||||
nodes = [Node(id=el[0], type=el[1]) for el in list(nodes_set)]
|
||||
|
||||
# Strict mode filtering
|
||||
if self.strict_mode and (self.allowed_nodes or self.allowed_relationships):
|
||||
if self.allowed_nodes:
|
||||
lower_allowed_nodes = [el.lower() for el in self.allowed_nodes]
|
||||
nodes = [
|
||||
node for node in nodes if node.type.lower() in lower_allowed_nodes
|
||||
]
|
||||
relationships = [
|
||||
rel
|
||||
for rel in relationships
|
||||
if rel.source.type.lower() in lower_allowed_nodes
|
||||
and rel.target.type.lower() in lower_allowed_nodes
|
||||
]
|
||||
if self.allowed_relationships:
|
||||
relationships = [
|
||||
rel
|
||||
for rel in relationships
|
||||
if rel.type.lower()
|
||||
in [el.lower() for el in self.allowed_relationships]
|
||||
]
|
||||
|
||||
return GraphDocument(nodes=nodes, relationships=relationships, source=document)
|
||||
|
||||
def convert_to_graph_documents(
|
||||
self, documents: Sequence[Document], config: Optional[RunnableConfig] = None
|
||||
) -> List[GraphDocument]:
|
||||
"""Convert a sequence of documents into graph documents.
|
||||
|
||||
Args:
|
||||
documents (Sequence[Document]): The original documents.
|
||||
**kwargs: Additional keyword arguments.
|
||||
|
||||
Returns:
|
||||
Sequence[GraphDocument]: The transformed documents as graphs.
|
||||
"""
|
||||
return [self.process_response(document, config) for document in documents]
|
||||
|
||||
async def aprocess_response(
|
||||
self, document: Document, config: Optional[RunnableConfig] = None
|
||||
) -> GraphDocument:
|
||||
"""
|
||||
Asynchronously processes a single document, transforming it into a
|
||||
graph document.
|
||||
"""
|
||||
text = document.page_content
|
||||
raw_schema = await self.chain.ainvoke({"input": text}, config=config)
|
||||
raw_schema = cast(Dict[Any, Any], raw_schema)
|
||||
nodes, relationships = _convert_to_graph_document(raw_schema)
|
||||
|
||||
if self.strict_mode and (self.allowed_nodes or self.allowed_relationships):
|
||||
if self.allowed_nodes:
|
||||
lower_allowed_nodes = [el.lower() for el in self.allowed_nodes]
|
||||
nodes = [
|
||||
node for node in nodes if node.type.lower() in lower_allowed_nodes
|
||||
]
|
||||
relationships = [
|
||||
rel
|
||||
for rel in relationships
|
||||
if rel.source.type.lower() in lower_allowed_nodes
|
||||
and rel.target.type.lower() in lower_allowed_nodes
|
||||
]
|
||||
if self.allowed_relationships:
|
||||
relationships = [
|
||||
rel
|
||||
for rel in relationships
|
||||
if rel.type.lower()
|
||||
in [el.lower() for el in self.allowed_relationships]
|
||||
]
|
||||
|
||||
return GraphDocument(nodes=nodes, relationships=relationships, source=document)
|
||||
|
||||
async def aconvert_to_graph_documents(
|
||||
self, documents: Sequence[Document], config: Optional[RunnableConfig] = None
|
||||
) -> List[GraphDocument]:
|
||||
"""
|
||||
Asynchronously convert a sequence of documents into graph documents.
|
||||
"""
|
||||
tasks = [
|
||||
asyncio.create_task(self.aprocess_response(document, config))
|
||||
for document in documents
|
||||
]
|
||||
results = await asyncio.gather(*tasks)
|
||||
return results
|
14
backend/database.py
Normal file
|
@ -0,0 +1,14 @@
|
|||
from sqlalchemy import create_engine, Column, Integer, String
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from envs import POSTGRES_DATABASE_URL
|
||||
|
||||
|
||||
engine = create_engine(
|
||||
POSTGRES_DATABASE_URL
|
||||
)
|
||||
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
Base = declarative_base()
|
10
backend/envs.py
Normal file
|
@ -0,0 +1,10 @@
|
|||
#POSTGRES DB TO TRACK USERS
|
||||
POSTGRES_DATABASE_URL = "postgresql+psycopg2://postgres:postgres@localhost:5432/surfsense"
|
||||
|
||||
# API KEY TO VERIFY
|
||||
API_SECRET_KEY = "surfsense"
|
||||
|
||||
# Your JWT secret and algorithm
|
||||
SECRET_KEY = "your_secret_key"
|
||||
ALGORITHM = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES = 720
|
14
backend/models.py
Normal file
|
@ -0,0 +1,14 @@
|
|||
from sqlalchemy import Column, Integer, String
|
||||
from database import Base
|
||||
from database import engine
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
username = Column(String, unique=True, index=True)
|
||||
hashed_password = Column(String)
|
||||
|
||||
# Create the database tables if they don't exist
|
||||
|
||||
User.metadata.create_all(bind=engine)
|
84
backend/prompts.py
Normal file
|
@ -0,0 +1,84 @@
|
|||
from langchain_core.prompts.prompt import PromptTemplate
|
||||
from langchain_core.prompts import ChatPromptTemplate
|
||||
from datetime import datetime, timezone
|
||||
|
||||
|
||||
DATE_TODAY = "Today's date is " + datetime.now(timezone.utc).astimezone().isoformat() + '\n'
|
||||
|
||||
CYPHER_QA_TEMPLATE = DATE_TODAY + """You are an assistant that helps to form nice and human understandable answers.
|
||||
The information part contains the provided information that you must use to construct an answer.
|
||||
The provided information is authoritative, you must never doubt it or try to use your internal knowledge to correct it.
|
||||
Make the answer sound as a response to the question. Do not mention that you based the result on the given information.
|
||||
Here are the examples:
|
||||
|
||||
Question: Website on which the most time was spend on?
|
||||
Context:[{'d.VisitedWebPageURL': 'https://stackoverflow.com/questions/59873698/the-default-export-is-not-a-react-component-in-page-nextjs', 'totalDuration': 8889167}]
|
||||
Helpful Answer: You visited https://stackoverflow.com/questions/59873698/the-default-export-is-not-a-react-component-in-page-nextjs for 8889167 milliseconds or 8889.167 seconds.
|
||||
|
||||
Follow this example when generating answers.
|
||||
If the provided information is empty, then and only then, return exactly 'don't know' as answer.
|
||||
|
||||
Information:
|
||||
{context}
|
||||
|
||||
Question: {question}
|
||||
Helpful Answer:"""
|
||||
|
||||
CYPHER_QA_PROMPT = PromptTemplate(
|
||||
input_variables=["context", "question"], template=CYPHER_QA_TEMPLATE
|
||||
)
|
||||
|
||||
SIMILARITY_SEARCH_RAG = DATE_TODAY + """You are an assistant for question-answering tasks.
|
||||
Use the following pieces of retrieved context to answer the question.
|
||||
If you don't know the answer, return exactly 'don't know' as answer.
|
||||
Question: {question}
|
||||
Context: {context}
|
||||
Answer:"""
|
||||
|
||||
|
||||
SIMILARITY_SEARCH_PROMPT = PromptTemplate(
|
||||
input_variables=["context", "question"], template=SIMILARITY_SEARCH_RAG
|
||||
)
|
||||
|
||||
# doc_extract_chain = DOCUMENT_METADATA_EXTRACTION_PROMT | structured_llm
|
||||
|
||||
|
||||
CYPHER_GENERATION_TEMPLATE = DATE_TODAY + """Task:Generate Cypher statement to query a graph database.
|
||||
Instructions:
|
||||
Use only the provided relationship types and properties in the schema.
|
||||
Do not use any other relationship types or properties that are not provided.
|
||||
|
||||
Schema:
|
||||
{schema}
|
||||
Note: Do not include any explanations or apologies in your responses.
|
||||
Do not respond to any questions that might ask anything else than for you to construct a Cypher statement.
|
||||
Do not include any text except the generated Cypher statement.
|
||||
|
||||
|
||||
The question is:
|
||||
{question}"""
|
||||
CYPHER_GENERATION_PROMPT = PromptTemplate(
|
||||
input_variables=["schema", "question"], template=CYPHER_GENERATION_TEMPLATE
|
||||
)
|
||||
|
||||
|
||||
DOC_DESCRIPTION_TEMPLATE = """Task:Give Detailed Description of the page content of the given document.
|
||||
Instructions:
|
||||
Provide as much details about metadata & page content as if you need to give human readable report of this Browsing session event.
|
||||
|
||||
Document:
|
||||
{document}
|
||||
"""
|
||||
DOC_DESCRIPTION_PROMPT = PromptTemplate(
|
||||
input_variables=["document"], template=DOC_DESCRIPTION_TEMPLATE
|
||||
)
|
||||
|
||||
|
||||
DOCUMENT_METADATA_EXTRACTION_SYSTEM_MESSAGE = DATE_TODAY + """You are a helpful assistant. You are given a Cypher statement result after quering the Neo4j graph database.
|
||||
Generate a very good Query that can be used to perform similarity search on the vector store of the Neo4j graph database"""
|
||||
|
||||
DOCUMENT_METADATA_EXTRACTION_PROMT = ChatPromptTemplate.from_messages([("system", DOCUMENT_METADATA_EXTRACTION_SYSTEM_MESSAGE), ("human", "{input}")])
|
||||
|
||||
|
||||
|
||||
|
41
backend/pydmodels.py
Normal file
|
@ -0,0 +1,41 @@
|
|||
from pydantic import BaseModel, Field
|
||||
from typing import List, Optional
|
||||
|
||||
class UserQuery(BaseModel):
|
||||
query: str
|
||||
neourl: str
|
||||
neouser: str
|
||||
neopass: str
|
||||
openaikey: str
|
||||
apisecretkey: str
|
||||
|
||||
class DescriptionResponse(BaseModel):
|
||||
response: str
|
||||
|
||||
|
||||
class DocMeta(BaseModel):
|
||||
BrowsingSessionId: Optional[str] = Field(default=None, description="BrowsingSessionId of Document")
|
||||
VisitedWebPageURL: Optional[str] = Field(default=None, description="VisitedWebPageURL of Document")
|
||||
VisitedWebPageTitle: Optional[str] = Field(default=None, description="VisitedWebPageTitle of Document")
|
||||
VisitedWebPageDateWithTimeInISOString: Optional[str] = Field(default=None, description="VisitedWebPageDateWithTimeInISOString of Document")
|
||||
VisitedWebPageReffererURL: Optional[str] = Field(default=None, description="VisitedWebPageReffererURL of Document")
|
||||
VisitedWebPageVisitDurationInMilliseconds: Optional[int] = Field(default=None, description="VisitedWebPageVisitDurationInMilliseconds of Document"),
|
||||
VisitedWebPageContent: Optional[str] = Field(default=None, description="Visited WebPage Content in markdown of Document")
|
||||
|
||||
class RetrivedDocListItem(BaseModel):
|
||||
metadata: DocMeta
|
||||
pageContent: str
|
||||
|
||||
class RetrivedDocList(BaseModel):
|
||||
documents: List[RetrivedDocListItem]
|
||||
neourl: str
|
||||
neouser: str
|
||||
neopass: str
|
||||
openaikey: str
|
||||
apisecretkey: str
|
||||
|
||||
|
||||
class UserQueryResponse(BaseModel):
|
||||
response: str
|
||||
relateddocs: List[DocMeta]
|
||||
|
8
backend/requirements.txt
Normal file
|
@ -0,0 +1,8 @@
|
|||
bcrypt
|
||||
cryptography
|
||||
fastapi
|
||||
python-jose
|
||||
python-multipart
|
||||
SQLAlchemy
|
||||
uvicorn
|
||||
passlib
|
322
backend/server.py
Normal file
|
@ -0,0 +1,322 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from langchain.chains import GraphCypherQAChain
|
||||
from langchain_community.graphs import Neo4jGraph
|
||||
from langchain_core.documents import Document
|
||||
from langchain_openai import OpenAIEmbeddings
|
||||
from langchain_community.vectorstores import Neo4jVector
|
||||
from envs import ACCESS_TOKEN_EXPIRE_MINUTES, ALGORITHM, API_SECRET_KEY, SECRET_KEY
|
||||
from prompts import CYPHER_QA_PROMPT, DOC_DESCRIPTION_PROMPT, SIMILARITY_SEARCH_PROMPT , CYPHER_GENERATION_PROMPT, DOCUMENT_METADATA_EXTRACTION_PROMT
|
||||
from pydmodels import DescriptionResponse, UserQuery, DocMeta, RetrivedDocList, UserQueryResponse
|
||||
from langchain_experimental.text_splitter import SemanticChunker
|
||||
|
||||
#Our Imps
|
||||
from LLMGraphTransformer import LLMGraphTransformer
|
||||
from langchain_openai import ChatOpenAI
|
||||
|
||||
# Auth Libs
|
||||
from fastapi import FastAPI, Depends, HTTPException, Request, status
|
||||
from sqlalchemy.orm import Session
|
||||
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
||||
from jose import JWTError, jwt
|
||||
from datetime import datetime, timedelta
|
||||
from passlib.context import CryptContext
|
||||
from models import User
|
||||
from database import SessionLocal, engine
|
||||
from pydantic import BaseModel
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@app.post("/")
|
||||
def get_user_query_response(data: UserQuery, response_model=UserQueryResponse):
|
||||
|
||||
if(data.apisecretkey != API_SECRET_KEY):
|
||||
raise HTTPException(status_code=401, detail="Unauthorized")
|
||||
|
||||
|
||||
query = data.query
|
||||
|
||||
graph = Neo4jGraph(url=data.neourl, username=data.neouser, password=data.neopass)
|
||||
|
||||
llm = ChatOpenAI(
|
||||
model="gpt-4o-mini",
|
||||
temperature=0,
|
||||
max_tokens=None,
|
||||
timeout=None,
|
||||
api_key=data.openaikey
|
||||
)
|
||||
|
||||
embeddings = OpenAIEmbeddings(
|
||||
model="text-embedding-ada-002",
|
||||
api_key=data.openaikey,
|
||||
)
|
||||
|
||||
|
||||
chain = GraphCypherQAChain.from_llm(
|
||||
graph=graph,
|
||||
cypher_prompt=CYPHER_GENERATION_PROMPT,
|
||||
cypher_llm=llm,
|
||||
verbose=True,
|
||||
validate_cypher=True,
|
||||
qa_prompt=CYPHER_QA_PROMPT ,
|
||||
qa_llm=llm,
|
||||
return_intermediate_steps=True,
|
||||
top_k=5,
|
||||
)
|
||||
|
||||
vector_index = Neo4jVector.from_existing_graph(
|
||||
embeddings,
|
||||
graph=graph,
|
||||
search_type="hybrid",
|
||||
node_label="Document",
|
||||
text_node_properties=["text"],
|
||||
embedding_node_property="embedding",
|
||||
)
|
||||
|
||||
docs = vector_index.similarity_search(query,k=5)
|
||||
|
||||
docstoreturn = []
|
||||
|
||||
for doc in docs:
|
||||
docstoreturn.append(
|
||||
DocMeta(
|
||||
BrowsingSessionId=doc.metadata["BrowsingSessionId"] if "BrowsingSessionId" in doc.metadata.keys() else "NOT AVAILABLE",
|
||||
VisitedWebPageURL=doc.metadata["VisitedWebPageURL"] if "VisitedWebPageURL" in doc.metadata.keys()else "NOT AVAILABLE",
|
||||
VisitedWebPageTitle=doc.metadata["VisitedWebPageTitle"] if "VisitedWebPageTitle" in doc.metadata.keys() else "NOT AVAILABLE",
|
||||
VisitedWebPageDateWithTimeInISOString= doc.metadata["VisitedWebPageDateWithTimeInISOString"] if "VisitedWebPageDateWithTimeInISOString" in doc.metadata.keys() else "NOT AVAILABLE",
|
||||
VisitedWebPageReffererURL= doc.metadata["VisitedWebPageReffererURL"] if "VisitedWebPageReffererURL" in doc.metadata.keys() else "NOT AVAILABLE",
|
||||
VisitedWebPageVisitDurationInMilliseconds= doc.metadata["VisitedWebPageVisitDurationInMilliseconds"] if "VisitedWebPageVisitDurationInMilliseconds" in doc.metadata.keys() else None,
|
||||
VisitedWebPageContent= doc.page_content if doc.page_content else "NOT AVAILABLE"
|
||||
)
|
||||
)
|
||||
|
||||
docstoreturn = [i for n, i in enumerate(docstoreturn) if i not in docstoreturn[n + 1:]]
|
||||
|
||||
|
||||
try:
|
||||
response = chain.invoke({"query": query})
|
||||
if "don't know" in response["result"]:
|
||||
raise Exception("No response from graph")
|
||||
|
||||
structured_llm = llm.with_structured_output(RetrivedDocList)
|
||||
doc_extract_chain = DOCUMENT_METADATA_EXTRACTION_PROMT | structured_llm
|
||||
|
||||
query = doc_extract_chain.invoke(response["intermediate_steps"][1]["context"])
|
||||
|
||||
docs = vector_index.similarity_search(query.searchquery,k=5)
|
||||
|
||||
docstoreturn = []
|
||||
|
||||
for doc in docs:
|
||||
docstoreturn.append(
|
||||
DocMeta(
|
||||
BrowsingSessionId=doc.metadata["BrowsingSessionId"] if "BrowsingSessionId" in doc.metadata.keys() else "NOT AVAILABLE",
|
||||
VisitedWebPageURL=doc.metadata["VisitedWebPageURL"] if "VisitedWebPageURL" in doc.metadata.keys()else "NOT AVAILABLE",
|
||||
VisitedWebPageTitle=doc.metadata["VisitedWebPageTitle"] if "VisitedWebPageTitle" in doc.metadata.keys() else "NOT AVAILABLE",
|
||||
VisitedWebPageDateWithTimeInISOString= doc.metadata["VisitedWebPageDateWithTimeInISOString"] if "VisitedWebPageDateWithTimeInISOString" in doc.metadata.keys() else "NOT AVAILABLE",
|
||||
VisitedWebPageReffererURL= doc.metadata["VisitedWebPageReffererURL"] if "VisitedWebPageReffererURL" in doc.metadata.keys() else "NOT AVAILABLE",
|
||||
VisitedWebPageVisitDurationInMilliseconds= doc.metadata["VisitedWebPageVisitDurationInMilliseconds"] if "VisitedWebPageVisitDurationInMilliseconds" in doc.metadata.keys() else None,
|
||||
VisitedWebPageContent= doc.page_content if doc.page_content else "NOT AVAILABLE"
|
||||
)
|
||||
)
|
||||
|
||||
docstoreturn = [i for n, i in enumerate(docstoreturn) if i not in docstoreturn[n + 1:]]
|
||||
|
||||
return UserQueryResponse(relateddocs=docstoreturn,response=response["result"])
|
||||
except:
|
||||
# Fallback to Similarity Search RAG
|
||||
searchchain = SIMILARITY_SEARCH_PROMPT | llm
|
||||
|
||||
response = searchchain.invoke({"question": query, "context": docs})
|
||||
|
||||
return UserQueryResponse(relateddocs=docstoreturn,response=response.content)
|
||||
|
||||
# DOC DESCRIPTION
|
||||
@app.post("/kb/doc")
|
||||
def get_doc_description(data: UserQuery, response_model=DescriptionResponse):
|
||||
if(data.apisecretkey != API_SECRET_KEY):
|
||||
raise HTTPException(status_code=401, detail="Unauthorized")
|
||||
|
||||
document = data.query
|
||||
llm = ChatOpenAI(
|
||||
model="gpt-4o-mini",
|
||||
temperature=0,
|
||||
max_tokens=None,
|
||||
timeout=None,
|
||||
api_key=data.openaikey
|
||||
)
|
||||
|
||||
descriptionchain = DOC_DESCRIPTION_PROMPT | llm
|
||||
|
||||
response = descriptionchain.invoke({"document": document})
|
||||
|
||||
return DescriptionResponse(response=response.content)
|
||||
|
||||
|
||||
# SAVE DOCS TO GRAPH DB
|
||||
@app.post("/kb/")
|
||||
def populate_graph(apires: RetrivedDocList):
|
||||
if(apires.apisecretkey != API_SECRET_KEY):
|
||||
raise HTTPException(status_code=401, detail="Unauthorized")
|
||||
|
||||
print("STARTED")
|
||||
# print(apires)
|
||||
graph = Neo4jGraph(url=apires.neourl, username=apires.neouser, password=apires.neopass)
|
||||
|
||||
llm = ChatOpenAI(
|
||||
model="gpt-4o-mini",
|
||||
temperature=0,
|
||||
max_tokens=None,
|
||||
timeout=None,
|
||||
api_key=apires.openaikey
|
||||
)
|
||||
|
||||
embeddings = OpenAIEmbeddings(
|
||||
model="text-embedding-ada-002",
|
||||
api_key=apires.openaikey,
|
||||
)
|
||||
|
||||
llm_transformer = LLMGraphTransformer(llm=llm)
|
||||
|
||||
raw_documents = []
|
||||
|
||||
for doc in apires.documents:
|
||||
raw_documents.append(Document(page_content=doc.pageContent, metadata=doc.metadata))
|
||||
|
||||
text_splitter = SemanticChunker(embeddings=embeddings)
|
||||
|
||||
documents = text_splitter.split_documents(raw_documents)
|
||||
graph_documents = llm_transformer.convert_to_graph_documents(documents)
|
||||
|
||||
|
||||
graph.add_graph_documents(
|
||||
graph_documents,
|
||||
baseEntityLabel=True,
|
||||
include_source=True
|
||||
)
|
||||
|
||||
print("FINISHED")
|
||||
|
||||
return {
|
||||
"success": "Graph Will be populated Shortly"
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
#AUTH CODE
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
|
||||
|
||||
# Recommended for Local Setups
|
||||
# origins = [
|
||||
# "http://localhost:3000", # Adjust the port if your frontend runs on a different one
|
||||
# "https://yourfrontenddomain.com",
|
||||
# ]
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"], # Allows all origins from the list
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"], # Allows all methods
|
||||
allow_headers=["*"], # Allows all headers
|
||||
)
|
||||
|
||||
# Dependency
|
||||
def get_db():
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
|
||||
class UserCreate(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
apisecretkey: str
|
||||
|
||||
def get_user_by_username(db: Session, username: str):
|
||||
return db.query(User).filter(User.username == username).first()
|
||||
|
||||
def create_user(db: Session, user: UserCreate):
|
||||
hashed_password = pwd_context.hash(user.password)
|
||||
db_user = User(username=user.username, hashed_password=hashed_password)
|
||||
db.add(db_user)
|
||||
db.commit()
|
||||
return "complete"
|
||||
|
||||
@app.post("/register")
|
||||
def register_user(user: UserCreate, db: Session = Depends(get_db)):
|
||||
if(user.apisecretkey != API_SECRET_KEY):
|
||||
raise HTTPException(status_code=401, detail="Unauthorized")
|
||||
|
||||
db_user = get_user_by_username(db, username=user.username)
|
||||
if db_user:
|
||||
raise HTTPException(status_code=400, detail="Username already registered")
|
||||
|
||||
del user.apisecretkey
|
||||
return create_user(db=db, user=user)
|
||||
|
||||
# Authenticate the user
|
||||
def authenticate_user(username: str, password: str, db: Session):
|
||||
user = db.query(User).filter(User.username == username).first()
|
||||
if not user:
|
||||
return False
|
||||
if not pwd_context.verify(password, user.hashed_password):
|
||||
return False
|
||||
return user
|
||||
|
||||
# Create access token
|
||||
def create_access_token(data: dict, expires_delta: timedelta | None = None):
|
||||
to_encode = data.copy()
|
||||
if expires_delta:
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
else:
|
||||
expire = datetime.utcnow() + timedelta(minutes=15)
|
||||
to_encode.update({"exp": expire})
|
||||
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
||||
return encoded_jwt
|
||||
|
||||
@app.post("/token")
|
||||
def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
|
||||
user = authenticate_user(form_data.username, form_data.password, db)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect username or password",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
access_token = create_access_token(
|
||||
data={"sub": user.username}, expires_delta=access_token_expires
|
||||
)
|
||||
return {"access_token": access_token, "token_type": "bearer"}
|
||||
|
||||
|
||||
def verify_token(token: str = Depends(oauth2_scheme)):
|
||||
try:
|
||||
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
||||
username: str = payload.get("sub")
|
||||
if username is None:
|
||||
raise HTTPException(status_code=403, detail="Token is invalid or expired")
|
||||
return payload
|
||||
except JWTError:
|
||||
raise HTTPException(status_code=403, detail="Token is invalid or expired")
|
||||
|
||||
@app.get("/verify-token/{token}")
|
||||
async def verify_user_token(token: str):
|
||||
verify_token(token=token)
|
||||
return {"message": "Token is valid"}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run(app, host="127.0.0.1", port=8000)
|
4
extension/.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
npm-debug.log
|
||||
node_modules/
|
||||
dist/
|
||||
tmp/
|
12
extension/.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"typescript.tsdk": "./node_modules/typescript/lib",
|
||||
"files.eol": "\n",
|
||||
"json.schemas": [
|
||||
{
|
||||
"fileMatch": [
|
||||
"/manifest.json"
|
||||
],
|
||||
"url": "http://json.schemastore.org/chrome-manifest"
|
||||
}
|
||||
]
|
||||
}
|
33
extension/.vscode/tasks.json
vendored
Normal file
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
// for the documentation about the tasks.json format
|
||||
"version": "2.0.0",
|
||||
"command": "npm",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "install",
|
||||
"type": "shell",
|
||||
"command": "npm",
|
||||
"args": ["install"]
|
||||
},
|
||||
{
|
||||
"label": "update",
|
||||
"type": "shell",
|
||||
"command": "npm",
|
||||
"args": ["update"]
|
||||
},
|
||||
{
|
||||
"label": "test",
|
||||
"type": "shell",
|
||||
"command": "npm",
|
||||
"args": ["run", "test"]
|
||||
},
|
||||
{
|
||||
"label": "build",
|
||||
"type": "shell",
|
||||
"group": "build",
|
||||
"command": "npm",
|
||||
"args": ["run", "watch"]
|
||||
}
|
||||
]
|
||||
}
|
21
extension/LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2018 Tomofumi Chiba
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
70
extension/README.md
Normal file
|
@ -0,0 +1,70 @@
|
|||
# Chrome Extension TypeScript Starter
|
||||
|
||||

|
||||
|
||||
Chrome Extension, TypeScript and Visual Studio Code
|
||||
|
||||
## Prerequisites
|
||||
|
||||
* [node + npm](https://nodejs.org/) (Current Version)
|
||||
|
||||
## Option
|
||||
|
||||
* [Visual Studio Code](https://code.visualstudio.com/)
|
||||
|
||||
## Includes the following
|
||||
|
||||
* TypeScript
|
||||
* Webpack
|
||||
* React
|
||||
* Jest
|
||||
* Example Code
|
||||
* Chrome Storage
|
||||
* Options Version 2
|
||||
* content script
|
||||
* count up badge number
|
||||
* background
|
||||
|
||||
## Project Structure
|
||||
|
||||
* src/typescript: TypeScript source files
|
||||
* src/assets: static files
|
||||
* dist: Chrome Extension directory
|
||||
* dist/js: Generated JavaScript files
|
||||
|
||||
## Setup
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
## Import as Visual Studio Code project
|
||||
|
||||
...
|
||||
|
||||
## Build
|
||||
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Build in watch mode
|
||||
|
||||
### terminal
|
||||
|
||||
```
|
||||
npm run watch
|
||||
```
|
||||
|
||||
### Visual Studio Code
|
||||
|
||||
Run watch mode.
|
||||
|
||||
type `Ctrl + Shift + B`
|
||||
|
||||
## Load extension to chrome
|
||||
|
||||
Load `dist` directory
|
||||
|
||||
## Test
|
||||
`npx jest` or `npm run test`
|
8
extension/jest.config.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
module.exports = {
|
||||
"roots": [
|
||||
"src"
|
||||
],
|
||||
"transform": {
|
||||
"^.+\\.ts$": ["ts-jest", { tsconfig: "tsconfig.test.json" }]
|
||||
},
|
||||
};
|
5033
extension/package-lock.json
generated
Normal file
52
extension/package.json
Normal file
|
@ -0,0 +1,52 @@
|
|||
{
|
||||
"name": "chrome-extension-typescript-starter",
|
||||
"version": "1.0.0",
|
||||
"description": "chrome-extension-typescript-starter",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"watch": "webpack --config webpack/webpack.dev.js --watch",
|
||||
"build": "webpack --config webpack/webpack.prod.js",
|
||||
"clean": "rimraf dist",
|
||||
"test": "npx jest",
|
||||
"style": "prettier --write \"src/**/*.{ts,tsx}\""
|
||||
},
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/chibat/chrome-extension-typescript-starter.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"dom-to-semantic-markdown": "^1.2.0",
|
||||
"postcss-loader": "^8.1.1",
|
||||
"react": "^18.2.0",
|
||||
"react-chrome-extension-router": "^1.4.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hooks-global-state": "^2.1.0",
|
||||
"react-router-dom": "^6.26.0",
|
||||
"react-toastify": "^10.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chrome": "0.0.158",
|
||||
"@types/jest": "^29.5.0",
|
||||
"@types/json2md": "^1.5.4",
|
||||
"@types/react": "^18.0.29",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"copy-webpack-plugin": "^9.0.1",
|
||||
"css-loader": "^7.1.2",
|
||||
"glob": "^7.1.6",
|
||||
"jest": "^29.5.0",
|
||||
"postcss": "^8.4.40",
|
||||
"prettier": "^2.2.1",
|
||||
"rimraf": "^3.0.2 ",
|
||||
"style-loader": "^4.0.0",
|
||||
"tailwindcss": "^3.4.7",
|
||||
"ts-jest": "^29.1.0",
|
||||
"ts-loader": "^8.0.0",
|
||||
"typescript": "^5.0.4",
|
||||
"webpack": "^5.76.0",
|
||||
"webpack-cli": "^4.0.0",
|
||||
"webpack-merge": "^5.0.0"
|
||||
}
|
||||
}
|
4622
extension/pnpm-lock.yaml
generated
Normal file
6
extension/postcss.config.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
BIN
extension/public/brain.png
Normal file
After Width: | Height: | Size: 5.7 KiB |
BIN
extension/public/icon-128.png
Normal file
After Width: | Height: | Size: 5.5 KiB |
BIN
extension/public/icon-16.png
Normal file
After Width: | Height: | Size: 587 B |
BIN
extension/public/icon-32.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
extension/public/icon-48.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
38
extension/public/manifest.json
Normal file
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"manifest_version": 3,
|
||||
|
||||
"name": "SurfSense",
|
||||
"description": "Extension to collect Browsing History for SurfSense.",
|
||||
"version": "0.0.1",
|
||||
|
||||
"action": {
|
||||
"default_popup": "popup.html"
|
||||
},
|
||||
"icons": {
|
||||
"16": "icon-16.png",
|
||||
"32": "icon-32.png",
|
||||
"48": "icon-48.png",
|
||||
"128": "icon-128.png"
|
||||
},
|
||||
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["<all_urls>"],
|
||||
"js": ["js/content_script.js"]
|
||||
}
|
||||
],
|
||||
|
||||
"background": {
|
||||
"service_worker": "js/background.js"
|
||||
},
|
||||
|
||||
"permissions": [
|
||||
"storage",
|
||||
"scripting",
|
||||
"unlimitedStorage"
|
||||
],
|
||||
|
||||
"host_permissions": [
|
||||
"<all_urls>"
|
||||
]
|
||||
}
|
12
extension/public/options.html
Normal file
|
@ -0,0 +1,12 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>My Test Extension Options</title>
|
||||
<script src="js/vendor.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script src="js/options.js"></script>
|
||||
</body>
|
||||
</html>
|
13
extension/public/popup.html
Normal file
|
@ -0,0 +1,13 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Getting Started Extension's Popup</title>
|
||||
<script src="js/vendor.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script src="js/popup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
9
extension/src/__tests__/sum.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { sum } from "../sum";
|
||||
|
||||
test("1 + 1 = 2", () => {
|
||||
expect(sum(1, 1)).toBe(2);
|
||||
});
|
||||
|
||||
test("1 + 2 != 2", () => {
|
||||
expect(sum(1, 2)).not.toBe(2);
|
||||
});
|
39
extension/src/assets/tailwind.css
Normal file
|
@ -0,0 +1,39 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
.loading{
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 80vh;
|
||||
}
|
||||
|
||||
body {
|
||||
min-width: 400px;
|
||||
}
|
||||
|
||||
.btn1 {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 25px;
|
||||
border: none;
|
||||
background-color: hotpink;
|
||||
color: white;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
@keyframes move {
|
||||
25% {
|
||||
transform: translateY(50px);
|
||||
}
|
||||
50% {
|
||||
background-color: dodgerblue;
|
||||
transform: translateY(0px);
|
||||
}
|
||||
75% {
|
||||
transform: translateY(-50px);
|
||||
}
|
||||
}
|
454
extension/src/background.ts
Normal file
|
@ -0,0 +1,454 @@
|
|||
import {
|
||||
initWebHistory,
|
||||
getRenderedHtml,
|
||||
initQueues,
|
||||
} from "./commons";
|
||||
import { WebHistory } from "./interfaces";
|
||||
|
||||
chrome.tabs.onCreated.addListener(async (tab: any) => {
|
||||
try {
|
||||
await initWebHistory(tab.id);
|
||||
await initQueues(tab.id);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
});
|
||||
|
||||
chrome.tabs.onUpdated.addListener(
|
||||
async (tabId: number, changeInfo: any, tab: any) => {
|
||||
if (changeInfo.status === "complete" && tab.url) {
|
||||
await initWebHistory(tab.id);
|
||||
await initQueues(tab.id);
|
||||
|
||||
const result = await chrome.scripting.executeScript({
|
||||
// @ts-ignore
|
||||
target: { tabId: tab.id },
|
||||
// @ts-ignore
|
||||
function: getRenderedHtml,
|
||||
});
|
||||
|
||||
let toPushInTabHistory = result[0].result; // const { renderedHtml, title, url, entryTime } = result[0].result;
|
||||
|
||||
let urlQueueListObj = await chrome.storage.local.get(["urlQueueList"]);
|
||||
let timeQueueListObj = await chrome.storage.local.get(["timeQueueList"]);
|
||||
|
||||
urlQueueListObj.urlQueueList
|
||||
.find((data: WebHistory) => data.tabsessionId === tabId)
|
||||
.urlQueue.push(toPushInTabHistory.url);
|
||||
timeQueueListObj.timeQueueList
|
||||
.find((data: WebHistory) => data.tabsessionId === tabId)
|
||||
.timeQueue.push(toPushInTabHistory.entryTime);
|
||||
|
||||
await chrome.storage.local.set({
|
||||
urlQueueList: urlQueueListObj.urlQueueList,
|
||||
});
|
||||
await chrome.storage.local.set({
|
||||
timeQueueList: timeQueueListObj.timeQueueList,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
chrome.tabs.onRemoved.addListener(async (tabId: number, removeInfo: object) => {
|
||||
let urlQueueListObj = await chrome.storage.local.get(["urlQueueList"]);
|
||||
let timeQueueListObj = await chrome.storage.local.get(["timeQueueList"]);
|
||||
if(urlQueueListObj.urlQueueList && timeQueueListObj.timeQueueList){
|
||||
|
||||
const urlQueueListToSave = urlQueueListObj.urlQueueList.map((element: WebHistory) => {
|
||||
if(element.tabsessionId !== tabId){
|
||||
return element
|
||||
}
|
||||
})
|
||||
const timeQueueListSave = timeQueueListObj.timeQueueList.map((element: WebHistory) => {
|
||||
if(element.tabsessionId !== tabId){
|
||||
return element
|
||||
}
|
||||
})
|
||||
await chrome.storage.local.set({
|
||||
urlQueueList: urlQueueListToSave.filter((item: any) => item),
|
||||
});
|
||||
await chrome.storage.local.set({
|
||||
timeQueueList: timeQueueListSave.filter((item: any) => item),
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
///// IGONRE THESE COMMENTS THESE CONTAINS SOME IDEAS THAT NEVER WORKED AS INTENTDED
|
||||
// await initWebHistory(tabId);
|
||||
// console.log("tab", tab);
|
||||
// if (tab.status === "loading") {
|
||||
// if (tab.url) {
|
||||
// const autotrackerFlag = await chrome.storage.local.get(["autoTracker"]);
|
||||
|
||||
// const lastUrlObj = await chrome.storage.local.get(["lastUrl"]);
|
||||
|
||||
// if (autotrackerFlag.autoTracker) {
|
||||
// if (lastUrlObj.lastUrl[tabId] !== "START") {
|
||||
// console.log("loading");
|
||||
// console.log(lastUrlObj.lastUrl[tabId]);
|
||||
// //update last entry duration
|
||||
// try {
|
||||
// const lastEntryTimeObj = await chrome.storage.local.get([
|
||||
// "lastEntryTime",
|
||||
// ]);
|
||||
// let webhistoryObj = await chrome.storage.local.get([
|
||||
// "webhistory",
|
||||
// ]);
|
||||
// const webHistoryLength = webhistoryObj.webhistory.find(
|
||||
// (data: WebHistory) => data.tabsessionId === tabId
|
||||
// ).tabHistory.length;
|
||||
|
||||
// if(webHistoryLength > 0){
|
||||
// webhistoryObj.webhistory.find(
|
||||
// (data: WebHistory) => data.tabsessionId === tabId
|
||||
// ).tabHistory[webHistoryLength - 1].duration =
|
||||
// Date.now() - lastEntryTimeObj.lastEntryTime[tabId];
|
||||
// }
|
||||
|
||||
// await chrome.storage.local.set({
|
||||
// webhistory: webhistoryObj.webhistory,
|
||||
// });
|
||||
// } catch (error) {
|
||||
// console.log(error);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// const autotrackerFlag = await chrome.storage.local.get(["autoTracker"]);
|
||||
// if (!autotrackerFlag.autoTracker) {
|
||||
// await initURlQueue(tab.id);
|
||||
// }
|
||||
|
||||
// const lastUrl = {
|
||||
// // @ts-ignore
|
||||
// [tab.id]: "START",
|
||||
// };
|
||||
|
||||
// // console.log(lastUrl);
|
||||
|
||||
// await chrome.storage.local.set({
|
||||
// lastUrl: lastUrl,
|
||||
// });
|
||||
|
||||
// const lastEntryTime = {
|
||||
// // @ts-ignore
|
||||
// [tab.id]: Date.now(),
|
||||
// };
|
||||
|
||||
// // console.log(lastUrl);
|
||||
|
||||
// await chrome.storage.local.set({
|
||||
// lastEntryTime: lastEntryTime,
|
||||
// });
|
||||
|
||||
// let webhistoryObj = await chrome.storage.local.get(["webhistory"]);
|
||||
// const webHistoryOfTabId = webhistoryObj.webhistory.filter(
|
||||
// (data: WebHistory) => {
|
||||
// return data.tabsessionId === tab.id;
|
||||
// }
|
||||
// );
|
||||
// let tabhistory = webHistoryOfTabId[0].tabHistory;
|
||||
|
||||
// if (tabhistory.length === 0) {
|
||||
// toPushInTabHistory.reffererUrl = "START";
|
||||
// } else {
|
||||
// toPushInTabHistory.reffererUrl = tabhistory[tabhistory.length - 1].url;
|
||||
// tabhistory[tabhistory.length - 1].duration =
|
||||
// toPushInTabHistory.entryTime -
|
||||
// tabhistory[tabhistory.length - 1].entryTime;
|
||||
// }
|
||||
|
||||
// tabhistory.push(toPushInTabHistory);
|
||||
|
||||
// //Update Webhistory
|
||||
// try {
|
||||
// webhistoryObj.webhistory.find(
|
||||
// (data: WebHistory) => data.tabsessionId === tab.id
|
||||
// ).tabHistory = tabhistory;
|
||||
|
||||
// await chrome.storage.local.set({
|
||||
// webhistory: webhistoryObj.webhistory,
|
||||
// });
|
||||
// } catch (error) {
|
||||
// console.log(error);
|
||||
// }
|
||||
|
||||
// const autotrackerFlag = await chrome.storage.local.get(["autoTracker"]);
|
||||
// if (autotrackerFlag.autoTracker) {
|
||||
// const result = await chrome.scripting.executeScript({
|
||||
// // @ts-ignore
|
||||
// target: { tabId: tab.id },
|
||||
// // @ts-ignore
|
||||
// function: getRenderedHtml,
|
||||
// });
|
||||
|
||||
// let toPushInTabHistory = result[0].result; // const { renderedHtml, title, url, entryTime } = result[0].result;
|
||||
|
||||
// // //Updates 'tabhistory'
|
||||
// let webhistoryObj = await chrome.storage.local.get(["webhistory"]);
|
||||
|
||||
// const webHistoryOfTabId = webhistoryObj.webhistory.filter(
|
||||
// (data: WebHistory) => {
|
||||
// return data.tabsessionId === tab.id;
|
||||
// }
|
||||
// );
|
||||
|
||||
// let tabhistory = webHistoryOfTabId[0].tabHistory;
|
||||
|
||||
// // let lastUrlObj = await chrome.storage.local.get(["lastUrl"]);
|
||||
// const lastEntryTimeObj = await chrome.storage.local.get([
|
||||
// "lastEntryTime",
|
||||
// ]);
|
||||
// lastEntryTimeObj.lastEntryTime[tabId] = Date.now();
|
||||
|
||||
// await chrome.storage.local.set({
|
||||
// lastEntryTime: lastEntryTimeObj.lastEntryTime,
|
||||
// });
|
||||
|
||||
// //When first entry
|
||||
// if (tabhistory.length === 0) {
|
||||
// let lastUrlObj = await chrome.storage.local.get(["lastUrl"]);
|
||||
// lastUrlObj.lastUrl[tabId] = tab.url;
|
||||
// await chrome.storage.local.set({
|
||||
// lastUrl: lastUrlObj.lastUrl,
|
||||
// });
|
||||
|
||||
// toPushInTabHistory.reffererUrl = "START";
|
||||
// toPushInTabHistory.entryTime = Date.now();
|
||||
// tabhistory.push(toPushInTabHistory);
|
||||
// try {
|
||||
// webhistoryObj.webhistory.find(
|
||||
// (data: WebHistory) => data.tabsessionId === tab.id
|
||||
// ).tabHistory = tabhistory;
|
||||
|
||||
// await chrome.storage.local.set({
|
||||
// webhistory: webhistoryObj.webhistory,
|
||||
// });
|
||||
// } catch (error) {
|
||||
// console.log(error);
|
||||
// }
|
||||
// } else {
|
||||
// const lastUrlObj = await chrome.storage.local.get(["lastUrl"]);
|
||||
|
||||
// toPushInTabHistory.reffererUrl = lastUrlObj.lastUrl[tabId];
|
||||
// toPushInTabHistory.entryTime = Date.now();
|
||||
// tabhistory.push(toPushInTabHistory);
|
||||
|
||||
// try {
|
||||
// webhistoryObj.webhistory.find(
|
||||
// (data: WebHistory) => data.tabsessionId === tab.id
|
||||
// ).tabHistory = tabhistory;
|
||||
// // console.log("webhistory",webhistoryObj);
|
||||
// await chrome.storage.local.set({
|
||||
// webhistory: webhistoryObj.webhistory,
|
||||
// });
|
||||
// } catch (error) {
|
||||
// console.log(error);
|
||||
// }
|
||||
|
||||
// lastUrlObj.lastUrl[tabId] = tab.url;
|
||||
// await chrome.storage.local.set({
|
||||
// lastUrl: lastUrlObj.lastUrl,
|
||||
// });
|
||||
// }
|
||||
// } else {
|
||||
// await initURlQueue(tab.id);
|
||||
// let urlQueue = await chrome.storage.local.get(["urlQueue"]);
|
||||
// // console.log("urlQueue", urlQueue);
|
||||
// urlQueue.urlQueue[tabId].push(tab.url)
|
||||
|
||||
// urlQueue.urlQueue[tabId] = [...new Set(urlQueue.urlQueue[tabId])]
|
||||
// await chrome.storage.local.set({
|
||||
// urlQueue: urlQueue.urlQueue,
|
||||
// });
|
||||
|
||||
// const lastEntryTimeObj = await chrome.storage.local.get([
|
||||
// "lastEntryTime",
|
||||
// ]);
|
||||
// lastEntryTimeObj.lastEntryTime[tabId] = Date.now();
|
||||
|
||||
// await chrome.storage.local.set({
|
||||
// lastEntryTime: lastEntryTimeObj.lastEntryTime,
|
||||
// });
|
||||
// }
|
||||
|
||||
// chrome.tabs.onRemoved.addListener(async (tabId: number, removeInfo: object) => {
|
||||
// const autotrackerFlag = await chrome.storage.local.get(["autoTracker"]);
|
||||
// //duration, referURL edge conditions
|
||||
// let webhistoryObj = await chrome.storage.local.get(["webhistory"]);
|
||||
// const webHistoryLength = webhistoryObj.webhistory.find(
|
||||
// (data: WebHistory) => data.tabsessionId === tabId
|
||||
// ).tabHistory.length;
|
||||
|
||||
// if (webHistoryLength > 0) {
|
||||
// if (
|
||||
// !webhistoryObj.webhistory.find(
|
||||
// (data: WebHistory) => data.tabsessionId === tabId
|
||||
// ).tabHistory[webHistoryLength - 1].duration
|
||||
// ) {
|
||||
// webhistoryObj.webhistory.find(
|
||||
// (data: WebHistory) => data.tabsessionId === tabId
|
||||
// ).tabHistory[webHistoryLength - 1].duration =
|
||||
// Date.now() -
|
||||
// webhistoryObj.webhistory.find(
|
||||
// (data: WebHistory) => data.tabsessionId === tabId
|
||||
// ).tabHistory[webHistoryLength - 1].entryTime;
|
||||
// }
|
||||
// }
|
||||
|
||||
// let urlQueueLocal = await chrome.storage.local.get(["urlQueue"]);
|
||||
// let timeQueueLocal = await chrome.storage.local.get(["timeQueue"]);
|
||||
// delete urlQueueLocal.urlQueue[tabId]
|
||||
// delete timeQueueLocal.timeQueue[tabId]
|
||||
// await chrome.storage.local.set({
|
||||
// urlQueue: urlQueueLocal.urlQueue,
|
||||
// });
|
||||
// await chrome.storage.local.set({
|
||||
// timeQueue: timeQueueLocal.timeQueue,
|
||||
// });
|
||||
|
||||
// // if (autotrackerFlag.autoTracker) {
|
||||
// // try {
|
||||
// // const lastEntryTimeObj = await chrome.storage.local.get([
|
||||
// // "lastEntryTime",
|
||||
// // ]);
|
||||
// // let webhistoryObj = await chrome.storage.local.get(["webhistory"]);
|
||||
// // const webHistoryLength = webhistoryObj.webhistory.find(
|
||||
// // (data: WebHistory) => data.tabsessionId === tabId
|
||||
// // ).tabHistory.length;
|
||||
|
||||
// // if (webHistoryLength > 0) {
|
||||
// // webhistoryObj.webhistory.find(
|
||||
// // (data: WebHistory) => data.tabsessionId === tabId
|
||||
// // ).tabHistory[webHistoryLength - 1].duration =
|
||||
// // Date.now() - lastEntryTimeObj.lastEntryTime[tabId];
|
||||
// // }
|
||||
|
||||
// // await chrome.storage.local.set({
|
||||
// // webhistory: webhistoryObj.webhistory,
|
||||
// // });
|
||||
// // } catch (error) {
|
||||
// // console.log(error);
|
||||
// // }
|
||||
// // } else {
|
||||
// // await initURlQueue(tabId);
|
||||
// // let urlQueue = await chrome.storage.local.get(["urlQueue"]);
|
||||
// // delete urlQueue.urlQueue[tabId];
|
||||
// // chrome.storage.local.set({
|
||||
// // urlQueue: urlQueue.urlQueue,
|
||||
// // });
|
||||
// // }
|
||||
// });
|
||||
|
||||
// if (tabhistory.length > 0) {
|
||||
// //updates duration of last entry in 'tabhistory'
|
||||
// tabhistory[tabhistory.length - 1].duration =
|
||||
// toPushInTabHistory.entryTime -
|
||||
// tabhistory[tabhistory.length - 1].entryTime;
|
||||
|
||||
// //Update refferer Url
|
||||
// toPushInTabHistory.reffererUrl = tabhistory[tabhistory.length - 1].url;
|
||||
|
||||
// if (tabhistory.length == 1) {
|
||||
// tabhistory[0].reffererUrl = "START";
|
||||
// }
|
||||
// }
|
||||
// tabhistory.push(toPushInTabHistory);
|
||||
|
||||
// //Set Updated tabhistory
|
||||
// try {
|
||||
// webhistoryObj.webhistory.find(
|
||||
// (data: WebHistory) => data.tabsessionId === tab.id
|
||||
// ).tabHistory = tabhistory;
|
||||
// // console.log("webhistory",webhistoryObj);
|
||||
// await chrome.storage.local.set({
|
||||
// webhistory: webhistoryObj.webhistory,
|
||||
// });
|
||||
// } catch (error) {
|
||||
// console.log(error);
|
||||
// }
|
||||
|
||||
// try {
|
||||
// let webhistoryObj = await chrome.storage.local.get(["webhistory"]);
|
||||
// const webHistoryLength = webhistoryObj.webhistory.find(
|
||||
// (data: WebHistory) => data.tabsessionId === tabId
|
||||
// ).tabHistory.length;
|
||||
// const lastWebPageEntryTime = webhistoryObj.webhistory.find(
|
||||
// (data: WebHistory) => data.tabsessionId === tabId
|
||||
// ).tabHistory[webHistoryLength - 1].entryTime;
|
||||
// webhistoryObj.webhistory.find(
|
||||
// (data: WebHistory) => data.tabsessionId === tabId
|
||||
// ).tabHistory[webHistoryLength - 1].duration =
|
||||
// Date.now() - lastWebPageEntryTime;
|
||||
// //Edge Condition of reffererUrl
|
||||
// if (webHistoryLength == 1) {
|
||||
// webhistoryObj.webhistory.find(
|
||||
// (data: WebHistory) => data.tabsessionId === tabId
|
||||
// ).tabHistory[0].reffererUrl = "START";
|
||||
// }
|
||||
// //Sets 'webhistory'
|
||||
// try {
|
||||
// await chrome.storage.local.set({ webhistory: webhistoryObj.webhistory });
|
||||
// // const result = await chrome.storage.local.get(["webhistory"]);
|
||||
// // console.log("webhistoryinRemoved",result);
|
||||
// } catch (error) {
|
||||
// console.log(error);
|
||||
// }
|
||||
// } catch (error) {
|
||||
// console.log(error);
|
||||
// }
|
||||
// await chrome.storage.local.set({ id: tab.id });
|
||||
// await chrome.storage.local.set({ tabhistory: [] });
|
||||
// const result = await chrome.storage.local.get(["id"]);
|
||||
// console.log(result);
|
||||
|
||||
// const tabid = await chrome.storage.local.get(["id"]);
|
||||
// const toPushinWebHostory = {
|
||||
// tabsessionId: tabid.id,
|
||||
// tabHistory: tabhistory,
|
||||
// };
|
||||
|
||||
// //Updates 'webhistory'
|
||||
// const webhistoryObj = await chrome.storage.local.get(["webhistory"]);
|
||||
// // console.log("WEBH", webhistoryObj);
|
||||
// const webhistory = webhistoryObj.webhistory;
|
||||
// webhistory.push(toPushinWebHostory);
|
||||
|
||||
// //Sets 'webhistory'
|
||||
// try {
|
||||
// await chrome.storage.local.set({ webhistory: webhistory });
|
||||
// const result = await chrome.storage.local.get(["webhistory"]);
|
||||
// console.log("RES",result)
|
||||
// } catch (error) {
|
||||
// console.log(error);
|
||||
// }
|
||||
|
||||
// try {
|
||||
// let lastUrlObj = await chrome.storage.local.get(["lastUrl"]);
|
||||
|
||||
// // console.log("Before Update", lastUrlObj);
|
||||
// if(tab.url !== lastUrlObj.lastUrl[tabId]){
|
||||
// // console.log("Before Update", lastUrlObj);
|
||||
// lastUrlObj.lastUrl[tabId] = tab.url;
|
||||
// // console.log("After Update", lastUrlObj);
|
||||
|
||||
// await chrome.storage.local.set({
|
||||
// lastUrl: lastUrlObj.lastUrl,
|
||||
// });
|
||||
|
||||
// //Update DURATION of old url
|
||||
// }
|
||||
|
||||
// // // const lastUrl = await chrome.storage.local.get(["lastUrl"]);
|
||||
|
||||
// // await chrome.storage.local.set({
|
||||
// // lastUrl: lastUrlObj.lastUrl,
|
||||
// // });
|
||||
// } catch (error) {
|
||||
// console.log(error);
|
||||
// }
|
126
extension/src/commons.tsx
Normal file
|
@ -0,0 +1,126 @@
|
|||
import { WebHistory } from "./interfaces";
|
||||
|
||||
export const emptyArr: any[] = [];
|
||||
|
||||
export const initQueues = async (tabId: number) => {
|
||||
let urlQueueListObj = await chrome.storage.local.get(["urlQueueList"]);
|
||||
let timeQueueListObj = await chrome.storage.local.get(["timeQueueList"]);
|
||||
|
||||
if(!urlQueueListObj.urlQueueList && !timeQueueListObj.timeQueueList){
|
||||
await chrome.storage.local.set({
|
||||
urlQueueList: [{tabsessionId: tabId, urlQueue: []}],
|
||||
});
|
||||
await chrome.storage.local.set({
|
||||
timeQueueList: [{tabsessionId: tabId, timeQueue: []}],
|
||||
});
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if(urlQueueListObj.urlQueueList && timeQueueListObj.timeQueueList){
|
||||
const isUrlQueueThere = urlQueueListObj.urlQueueList.find((data: WebHistory) => data.tabsessionId === tabId)
|
||||
const isTimeQueueThere = timeQueueListObj.timeQueueList.find((data: WebHistory) => data.tabsessionId === tabId)
|
||||
|
||||
if(!isUrlQueueThere){
|
||||
urlQueueListObj.urlQueueList.push({tabsessionId: tabId, urlQueue: []})
|
||||
|
||||
await chrome.storage.local.set({
|
||||
urlQueueList: urlQueueListObj.urlQueueList,
|
||||
});
|
||||
}
|
||||
|
||||
if(!isTimeQueueThere){
|
||||
timeQueueListObj.timeQueueList.push({tabsessionId: tabId, timeQueue: []})
|
||||
|
||||
await chrome.storage.local.set({
|
||||
timeQueueList: timeQueueListObj.timeQueueList,
|
||||
});
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
};
|
||||
|
||||
export function getRenderedHtml() {
|
||||
return {
|
||||
url: window.location.href,
|
||||
entryTime: Date.now(),
|
||||
title: document.title,
|
||||
renderedHtml: document.documentElement.outerHTML,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const initWebHistory = async (tabId: number) => {
|
||||
const result = await chrome.storage.local.get(["webhistory"]);
|
||||
|
||||
if (!result.webhistory) {
|
||||
await chrome.storage.local.set({ webhistory: emptyArr });
|
||||
return
|
||||
}
|
||||
|
||||
const ifIdExists = result.webhistory.find(
|
||||
(data: WebHistory) => data.tabsessionId === tabId
|
||||
);
|
||||
|
||||
|
||||
if (ifIdExists === undefined) {
|
||||
let webHistory = result.webhistory;
|
||||
const initData = {
|
||||
tabsessionId: tabId,
|
||||
tabHistory: emptyArr,
|
||||
};
|
||||
|
||||
webHistory.push(initData)
|
||||
|
||||
try {
|
||||
await chrome.storage.local.set({ webhistory: webHistory });
|
||||
return ;
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
export function toIsoString(date: Date) {
|
||||
var tzo = -date.getTimezoneOffset(),
|
||||
dif = tzo >= 0 ? '+' : '-',
|
||||
pad = function (num: number) {
|
||||
return (num < 10 ? '0' : '') + num;
|
||||
};
|
||||
|
||||
return date.getFullYear() +
|
||||
'-' + pad(date.getMonth() + 1) +
|
||||
'-' + pad(date.getDate()) +
|
||||
'T' + pad(date.getHours()) +
|
||||
':' + pad(date.getMinutes()) +
|
||||
':' + pad(date.getSeconds()) +
|
||||
dif + pad(Math.floor(Math.abs(tzo) / 60)) +
|
||||
':' + pad(Math.abs(tzo) % 60);
|
||||
}
|
||||
|
||||
|
||||
export const webhistoryToLangChainDocument = (tabId: number, tabHistory: any[]) => {
|
||||
let toSaveFinally = []
|
||||
for (let j = 0; j < tabHistory.length; j++) {
|
||||
|
||||
const mtadata = {
|
||||
"BrowsingSessionId": `${tabId}`,
|
||||
"VisitedWebPageURL": `${tabHistory[j].url}`,
|
||||
"VisitedWebPageTitle": `${tabHistory[j].title}`,
|
||||
"VisitedWebPageDateWithTimeInISOString": `${toIsoString(new Date(tabHistory[j].entryTime))}`,
|
||||
"VisitedWebPageReffererURL": `${tabHistory[j].reffererUrl}`,
|
||||
"VisitedWebPageVisitDurationInMilliseconds": tabHistory[j].duration,
|
||||
}
|
||||
|
||||
toSaveFinally.push({
|
||||
metadata: mtadata,
|
||||
pageContent: tabHistory[j].pageContentMarkdown
|
||||
})
|
||||
}
|
||||
|
||||
return toSaveFinally
|
||||
}
|
3
extension/src/content_script.tsx
Normal file
|
@ -0,0 +1,3 @@
|
|||
chrome.runtime.onMessage.addListener(function (msg, sender, sendResponse) {
|
||||
});
|
||||
|
2
extension/src/env.tsx
Normal file
|
@ -0,0 +1,2 @@
|
|||
export const API_SECRET_KEY = "surfsense"
|
||||
export const BACKEND_URL = "http://127.0.0.1:8000"
|
4
extension/src/interfaces.tsx
Normal file
|
@ -0,0 +1,4 @@
|
|||
export interface WebHistory {
|
||||
tabsessionId: number;
|
||||
tabHistory: any[];
|
||||
}
|
77
extension/src/options.tsx
Normal file
|
@ -0,0 +1,77 @@
|
|||
// import React, { useEffect, useState } from "react";
|
||||
// import { createRoot } from "react-dom/client";
|
||||
|
||||
// const Options = () => {
|
||||
// const [color, setColor] = useState<string>("");
|
||||
// const [status, setStatus] = useState<string>("");
|
||||
// const [like, setLike] = useState<boolean>(false);
|
||||
|
||||
// useEffect(() => {
|
||||
// // Restores select box and checkbox state using the preferences
|
||||
// // stored in chrome.storage.
|
||||
// chrome.storage.sync.get(
|
||||
// {
|
||||
// favoriteColor: "red",
|
||||
// likesColor: true,
|
||||
// },
|
||||
// (items) => {
|
||||
// setColor(items.favoriteColor);
|
||||
// setLike(items.likesColor);
|
||||
// }
|
||||
// );
|
||||
// }, []);
|
||||
|
||||
// const saveOptions = () => {
|
||||
// // Saves options to chrome.storage.sync.
|
||||
// chrome.storage.sync.set(
|
||||
// {
|
||||
// favoriteColor: color,
|
||||
// likesColor: like,
|
||||
// },
|
||||
// () => {
|
||||
// // Update status to let user know options were saved.
|
||||
// setStatus("Options saved.");
|
||||
// const id = setTimeout(() => {
|
||||
// setStatus("");
|
||||
// }, 1000);
|
||||
// return () => clearTimeout(id);
|
||||
// }
|
||||
// );
|
||||
// };
|
||||
|
||||
// return (
|
||||
// <>
|
||||
// <div>
|
||||
// Favorite color: <select
|
||||
// value={color}
|
||||
// onChange={(event) => setColor(event.target.value)}
|
||||
// >
|
||||
// <option value="red">red</option>
|
||||
// <option value="green">green</option>
|
||||
// <option value="blue">blue</option>
|
||||
// <option value="yellow">yellow</option>
|
||||
// </select>
|
||||
// </div>
|
||||
// <div>
|
||||
// <label>
|
||||
// <input
|
||||
// type="checkbox"
|
||||
// checked={like}
|
||||
// onChange={(event) => setLike(event.target.checked)}
|
||||
// />
|
||||
// I like colors.
|
||||
// </label>
|
||||
// </div>
|
||||
// <div>{status}</div>
|
||||
// <button onClick={saveOptions}>Save</button>
|
||||
// </>
|
||||
// );
|
||||
// };
|
||||
|
||||
// const root = createRoot(document.getElementById("root")!);
|
||||
|
||||
// root.render(
|
||||
// <React.StrictMode>
|
||||
// <Options />
|
||||
// </React.StrictMode>
|
||||
// );
|
75
extension/src/pages/EnvVarSettings.tsx
Normal file
|
@ -0,0 +1,75 @@
|
|||
import React, { useState } from "react";
|
||||
import { goTo } from "react-chrome-extension-router";
|
||||
import { Popup } from "../popup";
|
||||
export const FillEnvVariables = () => {
|
||||
const [neourl, setNeourl] = useState('');
|
||||
const [neouser, setNeouser] = useState('');
|
||||
const [neopass, setNeopass] = useState('');
|
||||
const [openaikey, setOpenaiKey] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const validateForm = () => {
|
||||
if (!neourl || !neouser || !neopass || !openaikey) {
|
||||
setError('All values are required');
|
||||
return false;
|
||||
}
|
||||
setError('');
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleSubmit = async (event: { preventDefault: () => void; }) => {
|
||||
event.preventDefault();
|
||||
if (!validateForm()) return;
|
||||
setLoading(true);
|
||||
|
||||
localStorage.setItem('neourl', neourl);
|
||||
localStorage.setItem('neouser', neouser);
|
||||
localStorage.setItem('neopass', neopass);
|
||||
localStorage.setItem('openaikey', openaikey);
|
||||
|
||||
setLoading(false);
|
||||
goTo(Popup)
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<section className="dark bg-gray-50 dark:bg-gray-900">
|
||||
<div className="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
|
||||
<a href="#" className="flex items-center mb-6 text-2xl font-semibold text-gray-900 dark:text-white">
|
||||
<img className="w-8 h-8 mr-2" src="./icon-128.png" alt="logo" />
|
||||
SurfSense
|
||||
</a>
|
||||
<div className="w-full bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700">
|
||||
<div className="p-6 space-y-4 md:space-y-6 sm:p-8">
|
||||
<h1 className="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white">
|
||||
Required Values
|
||||
</h1>
|
||||
<form className="space-y-4 md:space-y-6" onSubmit={handleSubmit}>
|
||||
<div>
|
||||
<label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Neo4J URL</label>
|
||||
<input type="text" value={neourl} onChange={(e) => setNeourl(e.target.value)} name="neourl" id="neourl" className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="name@company.com" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Neo4J Username</label>
|
||||
<input type="text" value={neouser} onChange={(e) => setNeouser(e.target.value)} name="neouser" id="neouser" className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="name@company.com" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Neo4J Password</label>
|
||||
<input type="text" value={neopass} onChange={(e) => setNeopass(e.target.value)} name="neopass" id="neopass" placeholder="••••••••" className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">OpenAI API Key</label>
|
||||
<input type="text" value={openaikey} onChange={(e) => setOpenaiKey(e.target.value)} name="openaikey" id="openaikey" className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="name@company.com" />
|
||||
</div>
|
||||
<button type="submit" className="mt-4 w-full text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">{loading ? 'Saving....' : 'Save & Proceed'}</button>
|
||||
{error && <p style={{ color: 'red' }}>{error}</p>}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
28
extension/src/pages/Loading.tsx
Normal file
|
@ -0,0 +1,28 @@
|
|||
import React from 'react'
|
||||
|
||||
const Loading = () => {
|
||||
return (
|
||||
<><div className="flex flex-col items-center justify-center px-6 pt-2 pb-12 mx-auto md:h-screen lg:py-0">
|
||||
<div className="flex items-center mb-6 text-2xl font-semibold text-gray-900 dark:text-white">
|
||||
<img className="w-8 h-8 mr-2" src="./icon-128.png" alt="logo" />
|
||||
SurfSense
|
||||
</div>
|
||||
<div className="loading">
|
||||
{"S A V I N G".split(" ").map((v, i) => (
|
||||
<button
|
||||
className="btn1"
|
||||
style={{ animation: `move linear 0.9s infinite ${i / 10}s` }}
|
||||
key={v}
|
||||
>
|
||||
{v}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
export default Loading
|
94
extension/src/pages/LoginForm.tsx
Normal file
|
@ -0,0 +1,94 @@
|
|||
import React, { useState } from "react";
|
||||
import { goTo } from "react-chrome-extension-router";
|
||||
import { Popup } from "../popup";
|
||||
|
||||
export const LoginForm = () => {
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
|
||||
const validateForm = () => {
|
||||
if (!username || !password) {
|
||||
setError('Username and password are required');
|
||||
return false;
|
||||
}
|
||||
setError('');
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleSubmit = async (event: { preventDefault: () => void; }) => {
|
||||
event.preventDefault();
|
||||
if (!validateForm()) return;
|
||||
setLoading(true);
|
||||
|
||||
const formDetails = new URLSearchParams();
|
||||
formDetails.append('username', username);
|
||||
formDetails.append('password', password);
|
||||
|
||||
try {
|
||||
const response = await fetch('http://localhost:8000/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: formDetails,
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
localStorage.setItem('token', data.access_token);
|
||||
goTo(Popup);
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
setError(errorData.detail || 'Authentication failed!');
|
||||
}
|
||||
} catch (error) {
|
||||
setLoading(false);
|
||||
setError('An error occurred. Please try again later.');
|
||||
}
|
||||
};
|
||||
|
||||
// const goToRegister = async () => {
|
||||
// console.log("Reg")
|
||||
// goTo(RegisterForm)
|
||||
// }
|
||||
return (
|
||||
<>
|
||||
<section className="dark bg-gray-50 dark:bg-gray-900">
|
||||
{/* <div onClick={() => clearMem}>CLEAR MEM</div> */}
|
||||
<div className="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
|
||||
<a href="#" className="flex items-center mb-6 text-2xl font-semibold text-gray-900 dark:text-white">
|
||||
<img className="w-8 h-8 mr-2" src={"./icon-128.png"} alt="logo" />
|
||||
SurfSense
|
||||
</a>
|
||||
<div className="w-full bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700">
|
||||
<div className="p-6 space-y-4 md:space-y-6 sm:p-8">
|
||||
<h1 className="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white">
|
||||
Sign in to your account
|
||||
</h1>
|
||||
<form className="space-y-4 md:space-y-6" onSubmit={handleSubmit}>
|
||||
<div>
|
||||
<label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Your email</label>
|
||||
<input type="text" value={username} onChange={(e) => setUsername(e.target.value)} name="email" id="email" className="bg-gray-50 border border-gray-300 text-gray-900 rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="name" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Password</label>
|
||||
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} name="password" id="password" placeholder="••••••••" className="bg-gray-50 border border-gray-300 text-gray-900 rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" />
|
||||
</div>
|
||||
<button type="submit" disabled={loading} className="w-full text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">{loading ? 'Logging in...' : 'Login'}</button>
|
||||
<p className="text-sm font-light text-gray-500 dark:text-gray-400">
|
||||
Don’t have an account yet? <a href="http://localhost:3000/signup" className="font-medium text-primary-600 hover:underline dark:text-primary-500" >Sign up</a>
|
||||
</p>
|
||||
{error && <p style={{ color: 'red' }}>{error}</p>}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
582
extension/src/popup.tsx
Normal file
|
@ -0,0 +1,582 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
goTo,
|
||||
Router,
|
||||
} from 'react-chrome-extension-router';
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { ToastContainer, toast } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import "./assets/tailwind.css"
|
||||
|
||||
import { convertHtmlToMarkdown } from "dom-to-semantic-markdown";
|
||||
import { WebHistory } from "./interfaces";
|
||||
import { webhistoryToLangChainDocument, getRenderedHtml, emptyArr } from "./commons";
|
||||
import Loading from "./pages/Loading";
|
||||
|
||||
import { LoginForm } from "./pages/LoginForm";
|
||||
import { FillEnvVariables } from "./pages/EnvVarSettings";
|
||||
import { API_SECRET_KEY, BACKEND_URL } from "./env";
|
||||
|
||||
|
||||
|
||||
export async function clearMem(): Promise<void> {
|
||||
try {
|
||||
|
||||
let result = await chrome.storage.local.get(["webhistory"]);
|
||||
|
||||
if (!result.webhistory) {
|
||||
return
|
||||
}
|
||||
|
||||
//Main Cleanup COde
|
||||
chrome.tabs.query({}, async (tabs) => {
|
||||
//Get Active Tabs Ids
|
||||
// console.log("Event Tabs",tabs)
|
||||
let actives = tabs.map((tab) => {
|
||||
if (tab.id) {
|
||||
return tab.id
|
||||
}
|
||||
})
|
||||
|
||||
actives = actives.filter((item: any) => item)
|
||||
|
||||
|
||||
//Only retain which is still active
|
||||
const newHistory = result.webhistory.map((element: any) => {
|
||||
//@ts-ignore
|
||||
if (actives.includes(element.tabsessionId)) {
|
||||
return element
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
await chrome.storage.local.set({ webhistory: newHistory.filter((item: any) => item) });
|
||||
|
||||
toast.info("History Store Deleted!", {
|
||||
position: "bottom-center"
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const Popup = () => {
|
||||
const [noOfWebPages, setNoOfWebPages] = useState<number>(0);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const verifyToken = async () => {
|
||||
const token = localStorage.getItem('token');
|
||||
console.log(token)
|
||||
try {
|
||||
const response = await fetch(`${BACKEND_URL}/verify-token/${token}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Token verification failed');
|
||||
}else{
|
||||
const NEO4JURL = localStorage.getItem('neourl');
|
||||
const NEO4JUSERNAME = localStorage.getItem('neouser');
|
||||
const NEO4JPASSWORD = localStorage.getItem('neopass');
|
||||
const OPENAIKEY = localStorage.getItem('openaikey');
|
||||
|
||||
const check = (NEO4JURL && NEO4JUSERNAME && NEO4JPASSWORD && OPENAIKEY)
|
||||
if(!check){
|
||||
goTo(FillEnvVariables);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
localStorage.removeItem('token');
|
||||
goTo(LoginForm);
|
||||
}
|
||||
|
||||
|
||||
|
||||
};
|
||||
|
||||
verifyToken();
|
||||
setLoading(false)
|
||||
}, []);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
async function onLoad() {
|
||||
try {
|
||||
chrome.storage.onChanged.addListener(
|
||||
(changes: any, areaName: string) => {
|
||||
if (changes.webhistory) {
|
||||
// console.log("changes.webhistory", changes.webhistory)
|
||||
const webhistory = changes.webhistory.newValue;
|
||||
|
||||
let sum = 0
|
||||
webhistory.forEach((element: any) => {
|
||||
sum = sum + element.tabHistory.length
|
||||
});
|
||||
|
||||
setNoOfWebPages(sum)
|
||||
}
|
||||
// console.log(changes)
|
||||
// console.log(areaName)
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
|
||||
const webhistoryObj = await chrome.storage.local.get(["webhistory"]);
|
||||
if (webhistoryObj.webhistory.length) {
|
||||
const webhistory = webhistoryObj.webhistory;
|
||||
|
||||
if (webhistoryObj) {
|
||||
let sum = 0
|
||||
webhistory.forEach((element: any) => {
|
||||
sum = sum + element.tabHistory.length
|
||||
});
|
||||
setNoOfWebPages(sum)
|
||||
}
|
||||
} else {
|
||||
setNoOfWebPages(0)
|
||||
}
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
onLoad()
|
||||
}, []);
|
||||
|
||||
const saveData = async () => {
|
||||
|
||||
try {
|
||||
// setLoading(true);
|
||||
|
||||
const webhistoryObj = await chrome.storage.local.get(["webhistory"]);
|
||||
const webhistory = webhistoryObj.webhistory;
|
||||
if (webhistory) {
|
||||
|
||||
let processedHistory: any[] = []
|
||||
let newHistoryAfterCleanup: any[] = []
|
||||
|
||||
webhistory.forEach((element: any) => {
|
||||
let tabhistory = element.tabHistory;
|
||||
for (let i = 0; i < tabhistory.length; i++) {
|
||||
tabhistory[i].pageContentMarkdown = convertHtmlToMarkdown(tabhistory[i].renderedHtml, {
|
||||
extractMainContent: true,
|
||||
enableTableColumnTracking: true,
|
||||
})
|
||||
|
||||
delete tabhistory[i].renderedHtml
|
||||
}
|
||||
|
||||
processedHistory.push({
|
||||
tabsessionId: element.tabsessionId,
|
||||
tabHistory: tabhistory,
|
||||
})
|
||||
|
||||
newHistoryAfterCleanup.push({
|
||||
tabsessionId: element.tabsessionId,
|
||||
tabHistory: emptyArr,
|
||||
})
|
||||
});
|
||||
|
||||
await chrome.storage.local.set({ webhistory: newHistoryAfterCleanup });
|
||||
let toSaveFinally = []
|
||||
|
||||
for (let i = 0; i < processedHistory.length; i++) {
|
||||
const markdownFormat = webhistoryToLangChainDocument(processedHistory[i].tabsessionId, processedHistory[i].tabHistory)
|
||||
toSaveFinally.push(...markdownFormat)
|
||||
}
|
||||
|
||||
// console.log("SAVING", toSaveFinally)
|
||||
|
||||
const toSend = {
|
||||
documents: toSaveFinally,
|
||||
neourl: localStorage.getItem('neourl'),
|
||||
neouser: localStorage.getItem('neouser'),
|
||||
neopass: localStorage.getItem('neopass'),
|
||||
openaikey: localStorage.getItem('openaikey'),
|
||||
apisecretkey: API_SECRET_KEY
|
||||
}
|
||||
|
||||
// console.log("toSend",toSend)
|
||||
|
||||
const requestOptions = {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(toSend),
|
||||
};
|
||||
|
||||
toast.info("Save Job Initiated.", {
|
||||
position: "bottom-center"
|
||||
});
|
||||
|
||||
const response = await fetch(`${BACKEND_URL}/kb/`, requestOptions);
|
||||
const res = await response.json();
|
||||
if (res.success) {
|
||||
toast.success("Save Job Completed.", {
|
||||
position: "bottom-center",
|
||||
autoClose: false
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// async function showMem(): Promise<void> {
|
||||
// // localStorage.removeItem('token');
|
||||
// // await chrome.storage.local.clear()
|
||||
// const webhistoryObj = await chrome.storage.local.get(["webhistory"]);
|
||||
// const urlQueue = await chrome.storage.local.get(["urlQueueList"]);
|
||||
// const timeQueue = await chrome.storage.local.get(["timeQueueList"]);
|
||||
// console.log("CURR MEM", webhistoryObj, urlQueue, timeQueue);
|
||||
|
||||
// // await chrome.storage.local.set({
|
||||
// // urlQueueList: urlQueueListObj.urlQueueList,
|
||||
// // });
|
||||
// // await chrome.storage.local.set({
|
||||
// // timeQueueList: timeQueueListObj.timeQueueList,
|
||||
// // });
|
||||
// // clearMem()
|
||||
// }
|
||||
|
||||
async function logOut(): Promise<void> {
|
||||
localStorage.removeItem('token');
|
||||
goTo(LoginForm)
|
||||
}
|
||||
|
||||
async function saveCurrSnapShot(): Promise<void> {
|
||||
chrome.tabs.query({ active: true, currentWindow: true }, async function (tabs) {
|
||||
const tab = tabs[0];
|
||||
if (tab.id) {
|
||||
// await initWebHistory(tab.id);
|
||||
// await initQueues(tab.id);
|
||||
const tabId: number = tab.id
|
||||
const result = await chrome.scripting.executeScript({
|
||||
// @ts-ignore
|
||||
target: { tabId: tab.id },
|
||||
// @ts-ignore
|
||||
function: getRenderedHtml,
|
||||
});
|
||||
|
||||
let toPushInTabHistory = result[0].result; // const { renderedHtml, title, url, entryTime } = result[0].result;
|
||||
|
||||
// //Updates 'tabhistory'
|
||||
let webhistoryObj = await chrome.storage.local.get(["webhistory"]);
|
||||
|
||||
const webHistoryOfTabId = webhistoryObj.webhistory.filter(
|
||||
(data: WebHistory) => {
|
||||
return data.tabsessionId === tab.id;
|
||||
}
|
||||
);
|
||||
|
||||
let tabhistory = webHistoryOfTabId[0].tabHistory;
|
||||
|
||||
|
||||
const urlQueueListObj = await chrome.storage.local.get(["urlQueueList"]);
|
||||
const timeQueueListObj = await chrome.storage.local.get(["timeQueueList"]);
|
||||
|
||||
const isUrlQueueThere = urlQueueListObj.urlQueueList.find((data: WebHistory) => data.tabsessionId === tabId)
|
||||
const isTimeQueueThere = timeQueueListObj.timeQueueList.find((data: WebHistory) => data.tabsessionId === tabId)
|
||||
|
||||
// console.log(isUrlQueueThere)
|
||||
// console.log(isTimeQueueThere)
|
||||
|
||||
// console.log(isTimeQueueThere.timeQueue[isTimeQueueThere.length - 1])
|
||||
|
||||
toPushInTabHistory.duration = toPushInTabHistory.entryTime - isTimeQueueThere.timeQueue[isTimeQueueThere.timeQueue.length - 1]
|
||||
if (isUrlQueueThere.urlQueue.length == 1) {
|
||||
toPushInTabHistory.reffererUrl = 'START'
|
||||
}
|
||||
if (isUrlQueueThere.urlQueue.length > 1) {
|
||||
toPushInTabHistory.reffererUrl = isUrlQueueThere.urlQueue[isUrlQueueThere.urlQueue.length - 2];
|
||||
}
|
||||
|
||||
tabhistory.push(toPushInTabHistory);
|
||||
|
||||
// console.log(toPushInTabHistory)
|
||||
|
||||
//Update Webhistory
|
||||
try {
|
||||
webhistoryObj.webhistory.find(
|
||||
(data: WebHistory) => data.tabsessionId === tab.id
|
||||
).tabHistory = tabhistory;
|
||||
|
||||
await chrome.storage.local.set({
|
||||
webhistory: webhistoryObj.webhistory,
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
|
||||
toast.success("Saved Snapshot !", {
|
||||
position: "bottom-center"
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
} else {
|
||||
return (
|
||||
<section className="dark bg-gray-50 dark:bg-gray-900">
|
||||
{/* <div onClick={() => showMem()}>ShowMem</div> */}
|
||||
<div className="flex flex-col items-center justify-center px-4 pt-4 pb-12 mx-auto md:h-screen lg:py-0">
|
||||
<div className="flex items-center mb-6 text-2xl font-semibold text-gray-900 dark:text-white">
|
||||
<img className="w-8 h-8 mr-2" src="./icon-128.png" alt="logo" />
|
||||
SurfSense
|
||||
</div>
|
||||
<div className="w-full bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700">
|
||||
<div className="p-6 space-y-4 md:space-y-6 sm:p-8">
|
||||
<div className="flex justify-between">
|
||||
<button type="button" onClick={() => goTo(FillEnvVariables)} className="px-3 py-2 text-xs font-medium text-center text-white bg-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" className="lucide lucide-settings"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z" /><circle cx="12" cy="12" r="3" /></svg>
|
||||
</button>
|
||||
<button type="button" onClick={() => logOut()} className="px-3 py-2 text-xs font-medium text-center text-white bg-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" className="lucide lucide-log-out"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" /><polyline points="16 17 21 12 16 7" /><line x1="21" x2="9" y1="12" y2="12" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="block max-w-sm p-4 bg-white border border-gray-200 rounded-lg shadow hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700">
|
||||
<div className="flex flex-col gap-4 justify-center items-center text-2xl font-semibold text-gray-900 dark:text-white">
|
||||
<img className="w-30 h-30 rounded-full" src="./brain.png" alt="brain" />
|
||||
<div>
|
||||
{noOfWebPages}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" className="w-full text-white bg-gradient-to-r from-red-400 via-red-500 to-red-600 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-red-300 dark:focus:ring-red-800 font-medium rounded-lg text-sm px-5 py-2.5 text-center" onClick={() => clearMem()}>Clear History Store</button>
|
||||
<button type="button" className="w-full text-gray-900 bg-gradient-to-r from-red-200 via-red-300 to-yellow-200 hover:bg-gradient-to-bl focus:ring-4 focus:outline-none focus:ring-red-100 dark:focus:ring-red-400 font-medium rounded-lg text-sm px-5 py-2.5 text-center" onClick={() => saveCurrSnapShot()}>Save Current Webpage SnapShot</button>
|
||||
<button type="button" className="w-full text-gray-900 bg-gradient-to-r from-teal-200 to-lime-200 hover:bg-gradient-to-l hover:from-teal-200 hover:to-lime-200 focus:ring-4 focus:outline-none focus:ring-lime-200 dark:focus:ring-teal-700 font-medium rounded-lg text-sm px-5 py-2.5 text-center me-2 mb-2" onClick={() => saveData()}>Save to SurfSense</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const root = createRoot(document.getElementById("root")!);
|
||||
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<Router>
|
||||
<Popup />
|
||||
</Router>
|
||||
<ToastContainer autoClose={2000} />
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
// chrome.tabs.query({ active: true, currentWindow: true }, async function (tabs) {
|
||||
// const tab = tabs[0];
|
||||
// if (tab.id) {
|
||||
// await initWebHistory(tab.id);
|
||||
// await initQueues(tab.id);
|
||||
// }
|
||||
// });
|
||||
// export const LoginForm = () => {
|
||||
// const [username, setUsername] = useState('');
|
||||
// const [password, setPassword] = useState('');
|
||||
// const [error, setError] = useState('');
|
||||
// const [loading, setLoading] = useState(false);
|
||||
|
||||
// // const navigate = useNavigate();
|
||||
|
||||
// const validateForm = () => {
|
||||
// if (!username || !password) {
|
||||
// setError('Username and password are required');
|
||||
// return false;
|
||||
// }
|
||||
// setError('');
|
||||
// return true;
|
||||
// };
|
||||
|
||||
// const handleSubmit = async (event: { preventDefault: () => void; }) => {
|
||||
// event.preventDefault();
|
||||
// if (!validateForm()) return;
|
||||
// setLoading(true);
|
||||
|
||||
// const formDetails = new URLSearchParams();
|
||||
// formDetails.append('username', username);
|
||||
// formDetails.append('password', password);
|
||||
|
||||
// try {
|
||||
// const response = await fetch('http://localhost:8000/token', {
|
||||
// method: 'POST',
|
||||
// headers: {
|
||||
// 'Content-Type': 'application/x-www-form-urlencoded',
|
||||
// },
|
||||
// body: formDetails,
|
||||
// });
|
||||
|
||||
// setLoading(false);
|
||||
|
||||
// if (response.ok) {
|
||||
// const data = await response.json();
|
||||
// await chrome.storage.local.set({
|
||||
// token: data.access_token,
|
||||
// });
|
||||
// // localStorage.setItem('token', data.access_token);
|
||||
// goTo(Popup);
|
||||
// } else {
|
||||
// const errorData = await response.json();
|
||||
// setError(errorData.detail || 'Authentication failed!');
|
||||
// }
|
||||
// } catch (error) {
|
||||
// setLoading(false);
|
||||
// setError('An error occurred. Please try again later.');
|
||||
// }
|
||||
// };
|
||||
|
||||
// const goToRegister = async () => {
|
||||
// console.log("Reg")
|
||||
// goTo(RegisterForm)
|
||||
// }
|
||||
// return (
|
||||
// <>
|
||||
// <section className="dark bg-gray-50 dark:bg-gray-900">
|
||||
// <div onClick={() => clearMem}>CLEAR MEM</div>
|
||||
// <div className="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
|
||||
// <a href="#" className="flex items-center mb-6 text-2xl font-semibold text-gray-900 dark:text-white">
|
||||
// <img className="w-8 h-8 mr-2" src={"./icon-128.png"} alt="logo" />
|
||||
// SurfSense
|
||||
// </a>
|
||||
// <div className="w-full bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700">
|
||||
// <div className="p-6 space-y-4 md:space-y-6 sm:p-8">
|
||||
// <h1 className="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white">
|
||||
// Sign in to your account
|
||||
// </h1>
|
||||
// <form className="space-y-4 md:space-y-6" onSubmit={handleSubmit}>
|
||||
// <div>
|
||||
// <label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Your email</label>
|
||||
// <input type="text" value={username} onChange={(e) => setUsername(e.target.value)} name="email" id="email" className="bg-gray-50 border border-gray-300 text-gray-900 rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="name" />
|
||||
// </div>
|
||||
// <div>
|
||||
// <label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Password</label>
|
||||
// <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} name="password" id="password" placeholder="••••••••" className="bg-gray-50 border border-gray-300 text-gray-900 rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" />
|
||||
// </div>
|
||||
// <button type="submit" disabled={loading} className="w-full text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">{loading ? 'Logging in...' : 'Login'}</button>
|
||||
// <p className="text-sm font-light text-gray-500 dark:text-gray-400">
|
||||
// Don’t have an account yet? <a href="#" className="font-medium text-primary-600 hover:underline dark:text-primary-500" onClick={() => goToRegister()}>Sign up</a>
|
||||
// </p>
|
||||
// {error && <p style={{ color: 'red' }}>{error}</p>}
|
||||
// </form>
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
// </section>
|
||||
// </>
|
||||
// );
|
||||
// }
|
||||
|
||||
// import { createGlobalState } from 'react-hooks-global-state';
|
||||
|
||||
// const initialState = { count: 0 };
|
||||
// const { useGlobalState } = createGlobalState(initialState);
|
||||
// const [count, setCount] = useGlobalState('count');
|
||||
|
||||
// setCount(v => v - 1);
|
||||
// let saveJobsObj = await chrome.storage.local.get(["savejobs"]);
|
||||
|
||||
// await chrome.storage.local.set({
|
||||
// savejobs: saveJobsObj.savejobs - 1,
|
||||
// });
|
||||
|
||||
// else{
|
||||
// toPushInTabHistory.reffererUrl = urlQueueLocal.urlQueue[tabId][urlQueueLocal.urlQueue[tabId].length - 1]
|
||||
// }
|
||||
|
||||
// if(!tabhistory[tabhistory.length - 1].duration){
|
||||
// tabhistory[tabhistory.length - 1].duration = Date.now() - timeQueueLocal.timeQueue[tabId][timeQueueLocal.timeQueue[tabId].length - 1]
|
||||
// }
|
||||
|
||||
|
||||
|
||||
// if (tabhistory.length === 0) {
|
||||
// toPushInTabHistory.duration = Date.now() - timeQueueLocal.timeQueue[tabId][timeQueueLocal.timeQueue[tabId].length - 1]
|
||||
// }
|
||||
// else {
|
||||
|
||||
// }
|
||||
|
||||
// const lastEntryTimeObj = await chrome.storage.local.get([
|
||||
// "lastEntryTime",
|
||||
// ]);
|
||||
|
||||
// const autotrackerFlag = await chrome.storage.local.get(["autoTracker"]);
|
||||
// // if (autotrackerFlag.autoTracker) {
|
||||
// // let urlQueue = await chrome.storage.local.get(["urlQueue"]);
|
||||
// // delete urlQueue.urlQueue[tabId];
|
||||
// // }
|
||||
|
||||
|
||||
// //When first entry
|
||||
// if (tabhistory.length === 0) {
|
||||
// let urlQueue = await chrome.storage.local.get(["urlQueue"]);
|
||||
|
||||
// if (autotrackerFlag.autoTracker) {
|
||||
// toPushInTabHistory.reffererUrl = "START";
|
||||
// try {
|
||||
// delete urlQueue.urlQueue[tabId];
|
||||
// } catch (error) {
|
||||
// console.log(error);
|
||||
// }
|
||||
// } else {
|
||||
|
||||
// if (urlQueue.urlQueue[tabId].length >= 2) {
|
||||
// toPushInTabHistory.reffererUrl = urlQueue.urlQueue[tabId][urlQueue.urlQueue[tabId].length - 2];
|
||||
// } else {
|
||||
// toPushInTabHistory.reffererUrl = "START";
|
||||
// }
|
||||
|
||||
// }
|
||||
// toPushInTabHistory.duration = Date.now() - lastEntryTimeObj.lastEntryTime[tabId];
|
||||
// tabhistory.push(toPushInTabHistory);
|
||||
// try {
|
||||
// webhistoryObj.webhistory.find(
|
||||
// (data: WebHistory) => data.tabsessionId === tab.id
|
||||
// ).tabHistory = tabhistory;
|
||||
// await chrome.storage.local.set({
|
||||
// webhistory: webhistoryObj.webhistory,
|
||||
// });
|
||||
// } catch (error) {
|
||||
// console.log(error);
|
||||
// }
|
||||
// } else {
|
||||
// if (autotrackerFlag.autoTracker) {
|
||||
// toPushInTabHistory.reffererUrl = tabhistory[tabhistory.length - 1].url;
|
||||
// } else {
|
||||
// let urlQueue = await chrome.storage.local.get(["urlQueue"]);
|
||||
// toPushInTabHistory.reffererUrl = urlQueue.urlQueue[tabId][urlQueue.urlQueue[tabId].length - 2];
|
||||
// }
|
||||
// if (!tabhistory[tabhistory.length - 1].duration) {
|
||||
// toPushInTabHistory.duration = Date.now() - tabhistory[tabhistory.length - 1].entryTime
|
||||
// }
|
||||
// toPushInTabHistory.duration = Date.now() - lastEntryTimeObj.lastEntryTime[tabId];
|
||||
// tabhistory.push(toPushInTabHistory);
|
||||
|
||||
// try {
|
||||
// webhistoryObj.webhistory.find(
|
||||
// (data: WebHistory) => data.tabsessionId === tab.id
|
||||
// ).tabHistory = tabhistory;
|
||||
// await chrome.storage.local.set({
|
||||
// webhistory: webhistoryObj.webhistory,
|
||||
// });
|
||||
// } catch (error) {
|
||||
// console.log(error);
|
||||
// }
|
||||
// }
|
3
extension/src/sum.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export function sum(x: number, y: number) {
|
||||
return x + y;
|
||||
}
|
60
extension/tailwind.config.js
Normal file
|
@ -0,0 +1,60 @@
|
|||
module.exports = {
|
||||
content: ["./src/*.{js,jsx,ts,tsx}"],
|
||||
darkMode: "selector",
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: {
|
||||
50: "#eff6ff",
|
||||
100: "#dbeafe",
|
||||
200: "#bfdbfe",
|
||||
300: "#93c5fd",
|
||||
400: "#60a5fa",
|
||||
500: "#3b82f6",
|
||||
600: "#2563eb",
|
||||
700: "#1d4ed8",
|
||||
800: "#1e40af",
|
||||
900: "#1e3a8a",
|
||||
950: "#172554",
|
||||
},
|
||||
},
|
||||
},
|
||||
fontFamily: {
|
||||
body: [
|
||||
"Inter",
|
||||
"ui-sans-serif",
|
||||
"system-ui",
|
||||
"-apple-system",
|
||||
"system-ui",
|
||||
"Segoe UI",
|
||||
"Roboto",
|
||||
"Helvetica Neue",
|
||||
"Arial",
|
||||
"Noto Sans",
|
||||
"sans-serif",
|
||||
"Apple Color Emoji",
|
||||
"Segoe UI Emoji",
|
||||
"Segoe UI Symbol",
|
||||
"Noto Color Emoji",
|
||||
],
|
||||
sans: [
|
||||
"Inter",
|
||||
"ui-sans-serif",
|
||||
"system-ui",
|
||||
"-apple-system",
|
||||
"system-ui",
|
||||
"Segoe UI",
|
||||
"Roboto",
|
||||
"Helvetica Neue",
|
||||
"Arial",
|
||||
"Noto Sans",
|
||||
"sans-serif",
|
||||
"Apple Color Emoji",
|
||||
"Segoe UI Emoji",
|
||||
"Segoe UI Symbol",
|
||||
"Noto Color Emoji",
|
||||
],
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
15
extension/tsconfig.json
Normal file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"target": "es6",
|
||||
"moduleResolution": "bundler",
|
||||
"module": "ES6",
|
||||
"esModuleInterop": true,
|
||||
"sourceMap": false,
|
||||
"rootDir": "src",
|
||||
"outDir": "dist/js",
|
||||
"noEmitOnError": true,
|
||||
"jsx": "react",
|
||||
"typeRoots": [ "node_modules/@types" ]
|
||||
}
|
||||
}
|
6
extension/tsconfig.test.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"moduleResolution": "node"
|
||||
}
|
||||
}
|
66
extension/webpack/webpack.common.js
Normal file
|
@ -0,0 +1,66 @@
|
|||
const webpack = require("webpack");
|
||||
const path = require("path");
|
||||
const CopyPlugin = require("copy-webpack-plugin");
|
||||
const srcDir = path.join(__dirname, "..", "src");
|
||||
|
||||
const tailwindcss = require('tailwindcss')
|
||||
const autoprefixer = require('autoprefixer')
|
||||
|
||||
module.exports = {
|
||||
entry: {
|
||||
popup: path.join(srcDir, 'popup.tsx'),
|
||||
background: path.join(srcDir, 'background.ts'),
|
||||
content_script: path.join(srcDir, 'content_script.tsx'),
|
||||
},
|
||||
output: {
|
||||
path: path.join(__dirname, "../dist/js"),
|
||||
filename: "[name].js",
|
||||
},
|
||||
optimization: {
|
||||
splitChunks: {
|
||||
name: "vendor",
|
||||
chunks(chunk) {
|
||||
return chunk.name !== 'background';
|
||||
}
|
||||
},
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
use: "ts-loader",
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
{
|
||||
test: /\.css$/i,
|
||||
use: [
|
||||
'style-loader',
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
importLoaders: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: 'postcss-loader', // postcss loader needed for tailwindcss
|
||||
options: {
|
||||
postcssOptions: {
|
||||
ident: 'postcss',
|
||||
plugins: [tailwindcss, autoprefixer],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
extensions: [".ts", ".tsx", ".js"],
|
||||
},
|
||||
plugins: [
|
||||
new CopyPlugin({
|
||||
patterns: [{ from: ".", to: "../", context: "public" }],
|
||||
options: {},
|
||||
}),
|
||||
],
|
||||
};
|
7
extension/webpack/webpack.dev.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
const { merge } = require('webpack-merge');
|
||||
const common = require('./webpack.common.js');
|
||||
|
||||
module.exports = merge(common, {
|
||||
devtool: 'inline-source-map',
|
||||
mode: 'development'
|
||||
});
|
6
extension/webpack/webpack.prod.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
const { merge } = require('webpack-merge');
|
||||
const common = require('./webpack.common.js');
|
||||
|
||||
module.exports = merge(common, {
|
||||
mode: 'production'
|
||||
});
|
36
web/.gitignore
vendored
Normal file
|
@ -0,0 +1,36 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
.yarn/install-state.gz
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
.env
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
36
web/README.md
Normal file
|
@ -0,0 +1,36 @@
|
|||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
8
web/app/chat/markdown.tsx
Normal file
|
@ -0,0 +1,8 @@
|
|||
import React from 'react';
|
||||
import MarkdownPreview from '@uiw/react-markdown-preview';
|
||||
|
||||
export default function MarkDownTest({source} : {source: string}) {
|
||||
return (
|
||||
<MarkdownPreview source={source} style={{ padding: 16 }} />
|
||||
)
|
||||
}
|
344
web/app/chat/page.tsx
Normal file
|
@ -0,0 +1,344 @@
|
|||
"use client";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import Image from "next/image";
|
||||
import logo from "@/public/SurfSense.png";
|
||||
import { FileCheck } from "lucide-react";
|
||||
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import MarkDownTest from "./markdown";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
type Document = {
|
||||
BrowsingSessionId: string;
|
||||
VisitedWebPageURL: string;
|
||||
VisitedWebPageTitle: string;
|
||||
VisitedWebPageDateWithTimeInISOString: string;
|
||||
VisitedWebPageReffererURL: string;
|
||||
VisitedWebPageVisitDurationInMilliseconds: number;
|
||||
VisitedWebPageContent: string;
|
||||
};
|
||||
|
||||
// type Description = {
|
||||
// response: string;
|
||||
// };
|
||||
|
||||
// type NormalResponse = {
|
||||
// response: string;
|
||||
// relateddocs: Document[];
|
||||
// };
|
||||
|
||||
// type ChatMessage = {
|
||||
// type: string;
|
||||
// userquery: string;
|
||||
// message: NormalResponse | string;
|
||||
// };
|
||||
|
||||
function ProtectedPage() {
|
||||
// const navigate = useNavigate();
|
||||
const router = useRouter();
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
|
||||
const [currentChat, setCurrentChat] = useState<any[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const verifyToken = async () => {
|
||||
const token = window.localStorage.getItem('token');
|
||||
// console.log(token)
|
||||
try {
|
||||
const response = await fetch(`${process.env.NEXT_PUBLIC_BACKEND_URL!}/verify-token/${token}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Token verification failed');
|
||||
}else{
|
||||
const NEO4JURL = localStorage.getItem('neourl');
|
||||
const NEO4JUSERNAME = localStorage.getItem('neouser');
|
||||
const NEO4JPASSWORD = localStorage.getItem('neopass');
|
||||
const OPENAIKEY = localStorage.getItem('openaikey');
|
||||
|
||||
const check = (NEO4JURL && NEO4JUSERNAME && NEO4JPASSWORD && OPENAIKEY)
|
||||
if(!check){
|
||||
router.push('/settings');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
window.localStorage.removeItem('token');
|
||||
router.push('/login');
|
||||
}
|
||||
};
|
||||
|
||||
verifyToken();
|
||||
}, [router]);
|
||||
|
||||
const handleSubmit = async (formData: any) => {
|
||||
setLoading(true);
|
||||
const query = formData.get("query");
|
||||
|
||||
if (!query) {
|
||||
console.log("Query cant be empty!!");
|
||||
return;
|
||||
}
|
||||
|
||||
const requestOptions = {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
query: query,
|
||||
neourl: localStorage.getItem('neourl'),
|
||||
neouser: localStorage.getItem('neouser'),
|
||||
neopass: localStorage.getItem('neopass'),
|
||||
openaikey: localStorage.getItem('openaikey'),
|
||||
apisecretkey: process.env.NEXT_PUBLIC_API_SECRET_KEY
|
||||
}),
|
||||
};
|
||||
|
||||
fetch(`${process.env.NEXT_PUBLIC_BACKEND_URL!}`, requestOptions)
|
||||
.then(res=>res.json())
|
||||
.then(data=> {
|
||||
let cur = currentChat;
|
||||
cur.push({
|
||||
type: "normal",
|
||||
userquery: query,
|
||||
message: data,
|
||||
});
|
||||
|
||||
|
||||
setCurrentChat([...cur]);
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const getDocDescription = async (document: Document) => {
|
||||
setLoading(true);
|
||||
const requestOptions = {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
query: JSON.stringify(document),
|
||||
neourl: localStorage.getItem('neourl'),
|
||||
neouser: localStorage.getItem('neouser'),
|
||||
neopass: localStorage.getItem('neopass'),
|
||||
openaikey: localStorage.getItem('openaikey'),
|
||||
apisecretkey: process.env.NEXT_PUBLIC_API_SECRET_KEY
|
||||
}),
|
||||
};
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_BACKEND_URL!}/kb/doc`,
|
||||
requestOptions
|
||||
);
|
||||
const res = await response.json();
|
||||
|
||||
let cur = currentChat;
|
||||
cur.push({
|
||||
type: "description",
|
||||
doctitle: document.VisitedWebPageTitle,
|
||||
message: res.response,
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
setCurrentChat([...cur]);
|
||||
// console.log(document);
|
||||
};
|
||||
|
||||
if (currentChat) {
|
||||
return (
|
||||
<>
|
||||
<div className="relative flex h-[calc(100vh_-_theme(spacing.16))] overflow-hidden mt-16">
|
||||
<div className="group w-full overflow-auto pl-0 peer-[[data-state=open]]:lg:pl-[250px] peer-[[data-state=open]]:xl:pl-[300px]">
|
||||
<div className="pb-[200px] pt-4 md:pt-10">
|
||||
<div className="mx-auto max-w-4xl px-4 flex flex-col gap-3">
|
||||
<div className="bg-background flex flex-col gap-2 rounded-lg border p-8">
|
||||
<h1 className="text-sm font-semibold">
|
||||
Welcome to SurfSense
|
||||
</h1>
|
||||
<p className="text-muted-foreground leading-normal">
|
||||
🧠 Ask Your Knowledge Graph Brain 🧠
|
||||
</p>
|
||||
</div>
|
||||
{currentChat.map((chat, index) => {
|
||||
// console.log("chat", chat);
|
||||
if (chat.type === "normal") {
|
||||
return (
|
||||
<div
|
||||
className="bg-background flex flex-col gap-2 rounded-lg border p-8"
|
||||
key={index}
|
||||
>
|
||||
<p className="text-3xl font-semibold">
|
||||
{chat.userquery}
|
||||
</p>
|
||||
<p className="font-sm font-semibold">
|
||||
SurfSense Response:
|
||||
</p>
|
||||
<MarkDownTest source={chat.message.response} />
|
||||
<p className="font-sm font-semibold">
|
||||
Related Browsing Sessions
|
||||
</p>
|
||||
|
||||
{
|
||||
//@ts-ignore
|
||||
chat.message.relateddocs.map((doc) => {
|
||||
return (
|
||||
<Collapsible className="border rounded-lg p-3">
|
||||
<CollapsibleTrigger className="flex justify-between gap-2 mb-2">
|
||||
<FileCheck />
|
||||
{doc.VisitedWebPageTitle}
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="flex flex-col gap-4">
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell className="font-medium">
|
||||
Browsing Session Id
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{doc.BrowsingSessionId}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="font-medium">
|
||||
URL
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{doc.VisitedWebPageURL}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="font-medium">
|
||||
Reffering URL
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{doc.VisitedWebPageReffererURL}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
<TableRow>
|
||||
<TableCell className="font-medium">
|
||||
Date & Time Visited
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{
|
||||
doc.VisitedWebPageDateWithTimeInISOString
|
||||
}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
<TableRow>
|
||||
<TableCell className="font-medium">
|
||||
Visit Duration (In Milliseconds)
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{
|
||||
doc.VisitedWebPageVisitDurationInMilliseconds
|
||||
}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => getDocDescription(doc)}
|
||||
className="text-gray-900 w-full hover:text-white border border-gray-800 hover:bg-gray-900 focus:ring-4 focus:outline-none focus:ring-gray-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center me-2 mb-2 dark:border-gray-600 dark:text-gray-400 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-800"
|
||||
>
|
||||
Get More Information
|
||||
</button>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (chat.type === "description") {
|
||||
return (
|
||||
<div
|
||||
className="bg-background flex flex-col gap-2 rounded-lg border p-8"
|
||||
key={index}
|
||||
>
|
||||
<p className="text-3xl font-semibold">
|
||||
{chat.doctitle}
|
||||
</p>
|
||||
<MarkDownTest source={chat.message} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
<div className="h-px w-full"></div>
|
||||
</div>
|
||||
<div className="from-muted/30 to-muted/30 animate-in dark:from-background/10 dark:to-background/80 inset-x-0 bottom-0 w-full duration-300 ease-in-out peer-[[data-state=open]]:group-[]:lg:pl-[250px] peer-[[data-state=open]]:group-[]:xl:pl-[300px] dark:from-10%">
|
||||
<div className="mx-auto sm:max-w-4xl sm:px-4">
|
||||
|
||||
<div className={loading ? "rounded-md p-4 w-full my-4" : "hidden"}>
|
||||
<div className="animate-pulse flex space-x-4">
|
||||
<div className="rounded-full bg-slate-700 h-10 w-10">
|
||||
</div>
|
||||
<div className="flex-1 space-y-6 py-1">
|
||||
<div className="h-2 bg-slate-700 rounded"></div>
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="h-2 bg-slate-700 rounded col-span-2"></div>
|
||||
<div className="h-2 bg-slate-700 rounded col-span-1"></div>
|
||||
</div>
|
||||
<div className="h-2 bg-slate-700 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-background space-y-4 border-t px-4 py-2 shadow-lg sm:rounded-t-xl sm:border md:py-4">
|
||||
<form action={handleSubmit}>
|
||||
<div className="bg-background relative flex max-h-60 w-full grow flex-col overflow-hidden px-8 sm:rounded-md sm:border sm:px-12">
|
||||
<Image
|
||||
className="inline-flex items-center justify-center whitespace-nowrap text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 border border-input shadow-sm hover:bg-accent hover:text-accent-foreground h-9 w-9 bg-background absolute left-0 top-[13px] size-8 rounded-full p-0 sm:left-4"
|
||||
src={logo}
|
||||
alt="aiicon"
|
||||
/>
|
||||
<span className="sr-only">New Chat</span>
|
||||
<textarea
|
||||
placeholder="Send a message."
|
||||
className="min-h-[60px] w-full resize-none bg-transparent px-4 py-[1.3rem] focus-within:outline-none sm:text-sm"
|
||||
name="query"
|
||||
></textarea>
|
||||
<div className="absolute right-0 top-[13px] sm:right-4">
|
||||
<button
|
||||
className="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground shadow hover:bg-primary/90 h-9 w-9"
|
||||
type="submit"
|
||||
data-state="closed"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 256 256"
|
||||
fill="currentColor"
|
||||
className="size-4"
|
||||
>
|
||||
<path d="M200 32v144a8 8 0 0 1-8 8H67.31l34.35 34.34a8 8 0 0 1-11.32 11.32l-48-48a8 8 0 0 1 0-11.32l48-48a8 8 0 0 1 11.32 11.32L67.31 168H184V32a8 8 0 0 1 16 0Z"></path>
|
||||
</svg>
|
||||
<span className="sr-only">Send message</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ProtectedPage;
|
BIN
web/app/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
69
web/app/globals.css
Normal file
|
@ -0,0 +1,69 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 0 0% 3.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 0 0% 3.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 0 0% 3.9%;
|
||||
--primary: 0 0% 9%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 0 0% 96.1%;
|
||||
--secondary-foreground: 0 0% 9%;
|
||||
--muted: 0 0% 96.1%;
|
||||
--muted-foreground: 0 0% 45.1%;
|
||||
--accent: 0 0% 96.1%;
|
||||
--accent-foreground: 0 0% 9%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 89.8%;
|
||||
--input: 0 0% 89.8%;
|
||||
--ring: 0 0% 3.9%;
|
||||
--radius: 0.5rem;
|
||||
--chart-1: 12 76% 61%;
|
||||
--chart-2: 173 58% 39%;
|
||||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 0 0% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 0 0% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 0 0% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 0 0% 9%;
|
||||
--secondary: 0 0% 14.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 0 0% 14.9%;
|
||||
--muted-foreground: 0 0% 63.9%;
|
||||
--accent: 0 0% 14.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 14.9%;
|
||||
--input: 0 0% 14.9%;
|
||||
--ring: 0 0% 83.1%;
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-2: 160 60% 45%;
|
||||
--chart-3: 30 80% 55%;
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-5: 340 75% 55%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
73
web/app/layout.tsx
Normal file
|
@ -0,0 +1,73 @@
|
|||
import type { Metadata } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
import { MainNavbar } from "@/components/homepage/NavBar";
|
||||
import { Toaster } from "@/components/ui/toaster"
|
||||
import { Footer } from "@/components/homepage/Footer";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "SurfSense - A Knowledge Graph Brain for World Wide Web Surfers.",
|
||||
description:
|
||||
"Save anything you see or browse on the Internet and save it to ask AI about it.",
|
||||
openGraph: {
|
||||
images: [
|
||||
{
|
||||
url: "https://surfsense.net/og-image.png",
|
||||
width: 1200,
|
||||
height: 627,
|
||||
alt: "SurfSense - A Knowledge Graph Brain for World Wide Web Surfers.",
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
site: "https://surfsense.net",
|
||||
creator: "https://surfsense.net",
|
||||
title: "SurfSense - A Knowledge Graph Brain for World Wide Web Surfers.",
|
||||
description:
|
||||
"Save anything you see or browse on the Internet and save it to ask AI about it.",
|
||||
images: [
|
||||
{
|
||||
url: "https://surfsense.net/og-image.png",
|
||||
width: 1200,
|
||||
height: 627,
|
||||
alt: "SurfSense - A Knowledge Graph Brain for World Wide Web Surfers.",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={inter.className}>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<MainNavbar />
|
||||
<div className="grow">
|
||||
{children}
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<Toaster />
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
9
web/app/login/page.tsx
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { LoginForm } from "@/components/logins/LoginForm"
|
||||
|
||||
const page = () => {
|
||||
return (
|
||||
<><LoginForm /></>
|
||||
)
|
||||
}
|
||||
|
||||
export default page
|
10
web/app/page.tsx
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { HomePage } from "@/components/homepage/HomePage";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<>
|
||||
<HomePage />
|
||||
</>
|
||||
|
||||
);
|
||||
}
|
80
web/app/settings/page.tsx
Normal file
|
@ -0,0 +1,80 @@
|
|||
"use client"
|
||||
import React, { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
const FillEnvVariables = () => {
|
||||
const [neourl, setNeourl] = useState('');
|
||||
const [neouser, setNeouser] = useState('');
|
||||
const [neopass, setNeopass] = useState('');
|
||||
const [openaikey, setOpenaiKey] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const validateForm = () => {
|
||||
if (!neourl || !neouser || !neopass || !openaikey) {
|
||||
setError('All values are required');
|
||||
return false;
|
||||
}
|
||||
setError('');
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleSubmit = async (event: { preventDefault: () => void; }) => {
|
||||
event.preventDefault();
|
||||
if (!validateForm()) return;
|
||||
setLoading(true);
|
||||
|
||||
localStorage.setItem('neourl', neourl);
|
||||
localStorage.setItem('neouser', neouser);
|
||||
localStorage.setItem('neopass', neopass);
|
||||
localStorage.setItem('openaikey', openaikey);
|
||||
|
||||
setLoading(false);
|
||||
router.push('/chat')
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<section className="bg-gray-50 dark:bg-gray-900">
|
||||
<div className="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
|
||||
<a href="#" className="flex items-center mb-6 text-2xl font-semibold text-gray-900 dark:text-white">
|
||||
<img className="w-8 h-8 mr-2" src="./icon-128.png" alt="logo" />
|
||||
SurfSense
|
||||
</a>
|
||||
<div className="w-full bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700">
|
||||
<div className="p-6 space-y-4 md:space-y-6 sm:p-8">
|
||||
<h1 className="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white">
|
||||
Required Values
|
||||
</h1>
|
||||
<form className="space-y-4 md:space-y-6" onSubmit={handleSubmit}>
|
||||
<div>
|
||||
<label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Neo4J URL</label>
|
||||
<input type="text" value={neourl} onChange={(e) => setNeourl(e.target.value)} name="neourl" id="neourl" className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="name@company.com" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Neo4J Username</label>
|
||||
<input type="text" value={neouser} onChange={(e) => setNeouser(e.target.value)} name="neouser" id="neouser" className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="name@company.com" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Neo4J Password</label>
|
||||
<input type="text" value={neopass} onChange={(e) => setNeopass(e.target.value)} name="neopass" id="neopass" placeholder="••••••••" className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">OpenAI API Key</label>
|
||||
<input type="text" value={openaikey} onChange={(e) => setOpenaiKey(e.target.value)} name="openaikey" id="openaikey" className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="name@company.com" />
|
||||
</div>
|
||||
<button type="submit" className="mt-4 w-full text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">{loading ? 'Saving....' : 'Save & Proceed'}</button>
|
||||
{error && <p style={{ color: 'red' }}>{error}</p>}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default FillEnvVariables
|
9
web/app/signup/page.tsx
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { RegisterForm } from "@/components/logins/RegisterForm"
|
||||
|
||||
const page = () => {
|
||||
return (
|
||||
<RegisterForm />
|
||||
)
|
||||
}
|
||||
|
||||
export default page
|
20
web/components.json
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.ts",
|
||||
"css": "app/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"examples": "@/components/examples",
|
||||
"blocks": "@/components/blocks"
|
||||
}
|
||||
}
|
43
web/components/Settings/EnvVarSettings.tsx
Normal file
|
@ -0,0 +1,43 @@
|
|||
import React from "react";
|
||||
import { useToast } from "../ui/use-toast";
|
||||
export const FillEnvVariables = () => {
|
||||
const { toast } = useToast()
|
||||
return (
|
||||
<section>
|
||||
<div className="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
|
||||
<a href="#" className="flex items-center mb-6 text-2xl font-semibold text-gray-900 dark:text-white">
|
||||
<img className="w-8 h-8 mr-2" src="./icon-128.png" alt="logo" />
|
||||
SurfSense
|
||||
</a>
|
||||
<div className="w-full bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700">
|
||||
<div className="p-6 space-y-4 md:space-y-6 sm:p-8">
|
||||
<h1 className="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white">
|
||||
Required Values
|
||||
</h1>
|
||||
<form className="space-y-4 md:space-y-6" action="#">
|
||||
<div>
|
||||
<label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Neo4J URL</label>
|
||||
<input type="email" name="email" id="email" className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="name@company.com" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Neo4J Username</label>
|
||||
<input type="email" name="email" id="email" className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="name@company.com" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Neo4J Password</label>
|
||||
<input type="password" name="password" id="password" placeholder="••••••••" className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">OpenAI API Key</label>
|
||||
<input type="email" name="email" id="email" className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="name@company.com" />
|
||||
</div>
|
||||
<button type="submit" className="mt-4 w-full text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">Save & Proceed</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
10
web/components/homepage/Footer.tsx
Normal file
|
@ -0,0 +1,10 @@
|
|||
export const Footer = () => {
|
||||
return (
|
||||
<footer className="mt-2 w-full md:flex overflow-y-hidden items-center justify-between gap-4 px-8 py-8 text-sm text-zinc-500 overflow-hidden text-center">
|
||||
<p>© 2024 SurfSense.net</p>
|
||||
<div className="flex gap-5 justify-around my-2">
|
||||
<a className="group/mail flex items-center" target="_blank" href="mailto:hi@dhravya.dev">Contact<svg className="group-hover/mail:opacity-100 opacity-0 transition hidden md:block" width="24px" height="24px" viewBox="-2.4 -2.4 28.80 28.80" fill="none" xmlns="http://www.w3.org/2000/svg" transform="matrix(1, 0, 0, 1, 0, 0)rotate(0)"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M7 17L17 7M17 7H8M17 7V16" stroke="currentColor" stroke-width="0.792" stroke-linecap="round" stroke-linejoin="round"></path> </g></svg></a><a className="group/twit flex items-center" target="_blank" href="https://twitter.com/supermemoryai">Twitter<svg className="group-hover/twit:opacity-100 opacity-0 transition hidden md:block" width="24px" height="24px" viewBox="-2.4 -2.4 28.80 28.80" fill="none" xmlns="http://www.w3.org/2000/svg" transform="matrix(1, 0, 0, 1, 0, 0)rotate(0)"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M7 17L17 7M17 7H8M17 7V16" stroke="currentColor" stroke-width="0.792" stroke-linecap="round" stroke-linejoin="round"></path> </g></svg></a><a className="group/git flex items-center" target="_blank" href="https://github.com/dhravya/supermemory">Github<svg className="group-hover/git:opacity-100 opacity-0 transition hidden md:block" width="24px" height="24px" viewBox="-2.4 -2.4 28.80 28.80" fill="none" xmlns="http://www.w3.org/2000/svg" transform="matrix(1, 0, 0, 1, 0, 0)rotate(0)"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M7 17L17 7M17 7H8M17 7V16" stroke="currentColor" stroke-width="0.792" stroke-linecap="round" stroke-linejoin="round"></path> </g></svg></a>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
}
|
43
web/components/homepage/HomePage.tsx
Normal file
|
@ -0,0 +1,43 @@
|
|||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import React from "react";
|
||||
import { AuroraBackground } from "../ui/aurora-background";
|
||||
|
||||
import icon from "../../public/SurfSense.png"
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
|
||||
export function HomePage() {
|
||||
return (
|
||||
<AuroraBackground>
|
||||
<motion.div
|
||||
initial={{ opacity: 0.0, y: 40 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{
|
||||
delay: 0.3,
|
||||
duration: 0.8,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
className="relative flex flex-col gap-4 items-center justify-center px-4"
|
||||
>
|
||||
<div className="flex items-center mb-4 text-5xl font-semibold text-gray-900 dark:text-white">
|
||||
<Image className="w-64 h-64 rounded-full" src={icon} alt="logo" />
|
||||
</div>
|
||||
<div className="text-3xl md:text-7xl font-bold dark:text-white text-center">
|
||||
SurfSense
|
||||
</div>
|
||||
{/* <div className="text-lg font-semibold dark:text-neutral-200">Beta v0.0.1</div> */}
|
||||
<div className="font-extralight text-base md:text-4xl dark:text-neutral-200 py-4">
|
||||
A Knowledge Graph 🧠 Brain 🧠 for World Wide Web Surfers.
|
||||
</div>
|
||||
<button className="relative inline-flex h-12 overflow-hidden rounded-full p-[1px] focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 focus:ring-offset-slate-50">
|
||||
<span className="absolute inset-[-1000%] animate-[spin_2s_linear_infinite] bg-[conic-gradient(from_90deg_at_50%_50%,#E2CBFF_0%,#393BB2_50%,#E2CBFF_100%)]" />
|
||||
<Link href={'/signup'} className="inline-flex h-full w-full cursor-pointer items-center justify-center rounded-full bg-slate-950 px-8 py-4 text-2xl font-medium text-white backdrop-blur-3xl">
|
||||
Sign Up
|
||||
</Link>
|
||||
</button>
|
||||
</motion.div>
|
||||
</AuroraBackground>
|
||||
);
|
||||
}
|
63
web/components/homepage/NavBar.tsx
Normal file
|
@ -0,0 +1,63 @@
|
|||
"use client";
|
||||
import React, { useState } from "react";
|
||||
import { HoveredLink, Menu, MenuItem, ProductItem } from "../ui/navbar-menu";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ThemeToggle } from "./theme-toggle";
|
||||
import Image from "next/image";
|
||||
import logo from "../../public/SurfSense.png"
|
||||
import Link from "next/link";
|
||||
|
||||
export function MainNavbar() {
|
||||
return (
|
||||
<div className="relative w-full flex items-center justify-around">
|
||||
<Navbar className="top-2 px-2" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Navbar({ className }: { className?: string }) {
|
||||
const [active, setActive] = useState<string | null>(null);
|
||||
return (
|
||||
<div
|
||||
className={cn("fixed top-10 inset-x-0 max-w-7xl mx-auto z-50", className)}
|
||||
>
|
||||
<Menu setActive={setActive}>
|
||||
<Link href={"/"} className="flex items-center text-2xl font-semibold text-gray-900 dark:text-white">
|
||||
<Image className="hidden sm:block w-8 h-8 mr-2" src={logo} alt="logo" />
|
||||
<span className="hidden md:block">SurfSense</span>
|
||||
</Link>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Link href={"/login"}>
|
||||
<button className="px-4 py-2 rounded-md border border-black bg-white text-black text-sm hover:shadow-[4px_4px_0px_0px_rgba(0,0,0)] transition duration-200">
|
||||
Log In
|
||||
</button>
|
||||
</Link>
|
||||
|
||||
<Link href={"/signup"}>
|
||||
<button className="px-4 py-2 rounded-md border border-black bg-white text-black text-sm hover:shadow-[4px_4px_0px_0px_rgba(0,0,0)] transition duration-200">
|
||||
Sign Up
|
||||
</button>
|
||||
</Link>
|
||||
|
||||
<Link href={"/settings"}>
|
||||
<button className="px-4 py-2 rounded-md border border-black bg-white text-black text-sm hover:shadow-[4px_4px_0px_0px_rgba(0,0,0)] transition duration-200">
|
||||
Settings
|
||||
</button>
|
||||
</Link>
|
||||
|
||||
<Link href={"/chat"} className="grow">
|
||||
<button className="px-4 py-2 rounded-md border border-black bg-white text-black text-sm hover:shadow-[4px_4px_0px_0px_rgba(0,0,0)] transition duration-200">
|
||||
🧠
|
||||
</button>
|
||||
</Link>
|
||||
|
||||
|
||||
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
}
|
40
web/components/homepage/theme-toggle.tsx
Normal file
|
@ -0,0 +1,40 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Moon, Sun } from "lucide-react"
|
||||
import { useTheme } from "next-themes"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
|
||||
export function ThemeToggle() {
|
||||
const { setTheme } = useTheme()
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button className="bg-transparent" variant="ghost" size="sm">
|
||||
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => setTheme("light")}>
|
||||
Light
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("dark")}>
|
||||
Dark
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("system")}>
|
||||
System
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
107
web/components/logins/LoginForm.tsx
Normal file
|
@ -0,0 +1,107 @@
|
|||
"use client"
|
||||
import React, { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
|
||||
|
||||
|
||||
export const LoginForm = () => {
|
||||
const router = useRouter();
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const validateForm = () => {
|
||||
if (!username || !password) {
|
||||
setError('Username and password are required');
|
||||
return false;
|
||||
}
|
||||
setError('');
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleSubmit = async (event: any) => {
|
||||
event.preventDefault();
|
||||
if (!validateForm()) return;
|
||||
setLoading(true);
|
||||
|
||||
const formDetails = new URLSearchParams();
|
||||
formDetails.append('username', username);
|
||||
formDetails.append('password', password);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${process.env.NEXT_PUBLIC_BACKEND_URL!}/token`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: formDetails,
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
window.localStorage.setItem('token', data.access_token);
|
||||
router.push('/chat');
|
||||
// navigate('/protected');
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
setError(errorData.detail || 'Authentication failed!');
|
||||
}
|
||||
} catch (error) {
|
||||
setLoading(false);
|
||||
setError('An error occurred. Please try again later.');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<section>
|
||||
<div className="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
|
||||
<a href="#" className="flex items-center mb-6 text-2xl font-semibold text-gray-900 dark:text-white">
|
||||
<img className="w-8 h-8 mr-2" src={"./icon-128.png"} alt="logo" />
|
||||
SurfSense
|
||||
</a>
|
||||
<div className="w-full bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700">
|
||||
<div className="p-6 space-y-4 md:space-y-6 sm:p-8">
|
||||
<h1 className="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white">
|
||||
Sign in to your account
|
||||
</h1>
|
||||
<form className="space-y-4 md:space-y-6" onSubmit={handleSubmit}>
|
||||
<div>
|
||||
<label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Username</label>
|
||||
<input name="email" id="email" value={username}
|
||||
onChange={(e) => setUsername(e.target.value)} className="bg-gray-50 border border-gray-300 text-gray-900 rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="name" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
id="password"
|
||||
placeholder="••••••••"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="bg-gray-50 border border-gray-300 text-gray-900 rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" className="w-full text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">
|
||||
{loading ? 'Logging in...' : 'Login'}
|
||||
</button>
|
||||
<p className="text-sm font-light text-gray-500 dark:text-gray-400">
|
||||
Don’t have an account yet? <Link href={"/signup"} className="font-medium text-primary-600 hover:underline dark:text-primary-500">Sign up</Link>
|
||||
</p>
|
||||
{error && <p style={{ color: 'red' }}>{error}</p>}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
112
web/components/logins/RegisterForm.tsx
Normal file
|
@ -0,0 +1,112 @@
|
|||
"use client"
|
||||
import React, { FormEvent, useState } from "react";
|
||||
import ReCAPTCHA from "react-google-recaptcha";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useToast } from "../ui/use-toast";
|
||||
import Link from "next/link";
|
||||
|
||||
export const RegisterForm = () => {
|
||||
const [captcha, setCaptcha] = useState<string | null>();
|
||||
const router = useRouter();
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [confpassword, setConfPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { toast } = useToast()
|
||||
|
||||
const validateForm = () => {
|
||||
if (!username || !password || !confpassword) {
|
||||
setError('Username and password are required');
|
||||
return false;
|
||||
}
|
||||
setError('');
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleSubmit = async (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
setLoading(true);
|
||||
|
||||
if (captcha) {
|
||||
if (!validateForm()) return;
|
||||
|
||||
try {
|
||||
|
||||
const toSend = {
|
||||
username: username,
|
||||
password: password,
|
||||
apisecretkey: process.env.NEXT_PUBLIC_API_SECRET_KEY!
|
||||
}
|
||||
|
||||
const response = await fetch(`${process.env.NEXT_PUBLIC_BACKEND_URL!}/register`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(toSend),
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
|
||||
if (response.ok) {
|
||||
toast({
|
||||
title: "Registered Successfully",
|
||||
description: "Redirecting to Login",
|
||||
})
|
||||
router.push('/login');
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
setError(errorData.detail || 'Authentication failed!');
|
||||
}
|
||||
} catch (error) {
|
||||
setLoading(false);
|
||||
setError('An error occurred. Please try again later.');
|
||||
}
|
||||
|
||||
} else {
|
||||
setError('Recaptcha Failed');
|
||||
}
|
||||
}
|
||||
return (
|
||||
<section>
|
||||
<div className="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
|
||||
<div className="flex items-center mb-6 text-2xl font-semibold text-gray-900 dark:text-white">
|
||||
<img className="w-8 h-8 mr-2" src="./icon-128.png" alt="logo" />
|
||||
SurfSense
|
||||
</div>
|
||||
<div className="w-full bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700">
|
||||
<div className="p-6 space-y-4 md:space-y-6 sm:p-8">
|
||||
<h1 className="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white">
|
||||
Create an account
|
||||
</h1>
|
||||
<form className="space-y-4 md:space-y-6" onSubmit={handleSubmit}>
|
||||
<div>
|
||||
<label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Your email</label>
|
||||
<input value={username}
|
||||
onChange={(e) => setUsername(e.target.value)} type="username" name="username" id="username" className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="name@company.com" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Password</label>
|
||||
<input value={password}
|
||||
onChange={(e) => setPassword(e.target.value)} type="password" name="password" id="password" placeholder="••••••••" className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Confirm password</label>
|
||||
<input value={confpassword}
|
||||
onChange={(e) => setConfPassword(e.target.value)}
|
||||
type="confirm-password" name="confpassword" id="confpassword" placeholder="••••••••" className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" />
|
||||
</div>
|
||||
<ReCAPTCHA sitekey={process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY!} className="mx-auto" onChange={setCaptcha} />
|
||||
<button type="submit" className="mt-4 w-full text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800"> {loading ? 'Creating...' : 'Create Account'}</button>
|
||||
<p className="text-sm font-light text-gray-500 dark:text-gray-400">
|
||||
Already have an account? <Link href={"/login"} className="font-medium text-primary-600 hover:underline dark:text-primary-500">Login here</Link>
|
||||
</p>
|
||||
{error && <p style={{ color: 'red' }}>{error}</p>}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
9
web/components/theme-provider.tsx
Normal file
|
@ -0,0 +1,9 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes"
|
||||
import { type ThemeProviderProps } from "next-themes/dist/types"
|
||||
|
||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
||||
}
|
54
web/components/ui/aurora-background.tsx
Normal file
|
@ -0,0 +1,54 @@
|
|||
"use client";
|
||||
import { cn } from "@/lib/utils";
|
||||
import React, { ReactNode } from "react";
|
||||
|
||||
interface AuroraBackgroundProps extends React.HTMLProps<HTMLDivElement> {
|
||||
children: ReactNode;
|
||||
showRadialGradient?: boolean;
|
||||
}
|
||||
|
||||
export const AuroraBackground = ({
|
||||
className,
|
||||
children,
|
||||
showRadialGradient = true,
|
||||
...props
|
||||
}: AuroraBackgroundProps) => {
|
||||
return (
|
||||
<main>
|
||||
<div
|
||||
className={cn(
|
||||
"relative flex flex-col h-[100vh] items-center justify-center bg-zinc-50 dark:bg-zinc-900 text-slate-950 transition-bg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="absolute inset-0 overflow-hidden">
|
||||
<div
|
||||
// I'm sorry but this is what peak developer performance looks like // trigger warning
|
||||
className={cn(
|
||||
`
|
||||
[--white-gradient:repeating-linear-gradient(100deg,var(--white)_0%,var(--white)_7%,var(--transparent)_10%,var(--transparent)_12%,var(--white)_16%)]
|
||||
[--dark-gradient:repeating-linear-gradient(100deg,var(--black)_0%,var(--black)_7%,var(--transparent)_10%,var(--transparent)_12%,var(--black)_16%)]
|
||||
[--aurora:repeating-linear-gradient(100deg,var(--blue-500)_10%,var(--indigo-300)_15%,var(--blue-300)_20%,var(--violet-200)_25%,var(--blue-400)_30%)]
|
||||
[background-image:var(--white-gradient),var(--aurora)]
|
||||
dark:[background-image:var(--dark-gradient),var(--aurora)]
|
||||
[background-size:300%,_200%]
|
||||
[background-position:50%_50%,50%_50%]
|
||||
filter blur-[10px] invert dark:invert-0
|
||||
after:content-[""] after:absolute after:inset-0 after:[background-image:var(--white-gradient),var(--aurora)]
|
||||
after:dark:[background-image:var(--dark-gradient),var(--aurora)]
|
||||
after:[background-size:200%,_100%]
|
||||
after:animate-aurora after:[background-attachment:fixed] after:mix-blend-difference
|
||||
pointer-events-none
|
||||
absolute -inset-[10px] opacity-50 will-change-transform`,
|
||||
|
||||
showRadialGradient &&
|
||||
`[mask-image:radial-gradient(ellipse_at_100%_0%,black_10%,var(--transparent)_70%)]`
|
||||
)}
|
||||
></div>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
};
|
56
web/components/ui/button.tsx
Normal file
|
@ -0,0 +1,56 @@
|
|||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button, buttonVariants }
|
11
web/components/ui/collapsible.tsx
Normal file
|
@ -0,0 +1,11 @@
|
|||
"use client"
|
||||
|
||||
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
|
||||
|
||||
const Collapsible = CollapsiblePrimitive.Root
|
||||
|
||||
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
|
||||
|
||||
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
|
||||
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
200
web/components/ui/dropdown-menu.tsx
Normal file
|
@ -0,0 +1,200 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||
import { Check, ChevronRight, Circle } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root
|
||||
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
||||
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
||||
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
||||
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
||||
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
||||
|
||||
const DropdownMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto h-4 w-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
))
|
||||
DropdownMenuSubTrigger.displayName =
|
||||
DropdownMenuPrimitive.SubTrigger.displayName
|
||||
|
||||
const DropdownMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuSubContent.displayName =
|
||||
DropdownMenuPrimitive.SubContent.displayName
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
))
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
||||
|
||||
const DropdownMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
||||
|
||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
))
|
||||
DropdownMenuCheckboxItem.displayName =
|
||||
DropdownMenuPrimitive.CheckboxItem.displayName
|
||||
|
||||
const DropdownMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Circle className="h-2 w-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
))
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
||||
|
||||
const DropdownMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-semibold",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
||||
|
||||
const DropdownMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
||||
|
||||
const DropdownMenuShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
}
|
104
web/components/ui/lamp.tsx
Normal file
|
@ -0,0 +1,104 @@
|
|||
"use client";
|
||||
import React from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export default function LampDemo() {
|
||||
return (
|
||||
<LampContainer>
|
||||
<motion.h1
|
||||
initial={{ opacity: 0.5, y: 100 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{
|
||||
delay: 0.3,
|
||||
duration: 0.8,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
className="mt-8 bg-gradient-to-br from-slate-300 to-slate-500 py-4 bg-clip-text text-center text-4xl font-medium tracking-tight text-transparent md:text-7xl"
|
||||
>
|
||||
Build lamps <br /> the right way
|
||||
</motion.h1>
|
||||
</LampContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export const LampContainer = ({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative flex min-h-screen flex-col items-center justify-center overflow-hidden bg-slate-950 w-full rounded-md z-0",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="relative flex w-full flex-1 scale-y-125 items-center justify-center isolate z-0 ">
|
||||
<motion.div
|
||||
initial={{ opacity: 0.5, width: "15rem" }}
|
||||
whileInView={{ opacity: 1, width: "30rem" }}
|
||||
transition={{
|
||||
delay: 0.3,
|
||||
duration: 0.8,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
style={{
|
||||
backgroundImage: `conic-gradient(var(--conic-position), var(--tw-gradient-stops))`,
|
||||
}}
|
||||
className="absolute inset-auto right-1/2 h-56 overflow-visible w-[30rem] bg-gradient-conic from-cyan-500 via-transparent to-transparent text-white [--conic-position:from_70deg_at_center_top]"
|
||||
>
|
||||
<div className="absolute w-[100%] left-0 bg-slate-950 h-40 bottom-0 z-20 [mask-image:linear-gradient(to_top,white,transparent)]" />
|
||||
<div className="absolute w-40 h-[100%] left-0 bg-slate-950 bottom-0 z-20 [mask-image:linear-gradient(to_right,white,transparent)]" />
|
||||
</motion.div>
|
||||
<motion.div
|
||||
initial={{ opacity: 0.5, width: "15rem" }}
|
||||
whileInView={{ opacity: 1, width: "30rem" }}
|
||||
transition={{
|
||||
delay: 0.3,
|
||||
duration: 0.8,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
style={{
|
||||
backgroundImage: `conic-gradient(var(--conic-position), var(--tw-gradient-stops))`,
|
||||
}}
|
||||
className="absolute inset-auto left-1/2 h-56 w-[30rem] bg-gradient-conic from-transparent via-transparent to-cyan-500 text-white [--conic-position:from_290deg_at_center_top]"
|
||||
>
|
||||
<div className="absolute w-40 h-[100%] right-0 bg-slate-950 bottom-0 z-20 [mask-image:linear-gradient(to_left,white,transparent)]" />
|
||||
<div className="absolute w-[100%] right-0 bg-slate-950 h-40 bottom-0 z-20 [mask-image:linear-gradient(to_top,white,transparent)]" />
|
||||
</motion.div>
|
||||
<div className="absolute top-1/2 h-48 w-full translate-y-12 scale-x-150 bg-slate-950 blur-2xl"></div>
|
||||
<div className="absolute top-1/2 z-50 h-48 w-full bg-transparent opacity-10 backdrop-blur-md"></div>
|
||||
<div className="absolute inset-auto z-50 h-36 w-[28rem] -translate-y-1/2 rounded-full bg-cyan-500 opacity-50 blur-3xl"></div>
|
||||
<motion.div
|
||||
initial={{ width: "8rem" }}
|
||||
whileInView={{ width: "16rem" }}
|
||||
transition={{
|
||||
delay: 0.3,
|
||||
duration: 0.8,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
className="absolute inset-auto z-30 h-36 w-64 -translate-y-[6rem] rounded-full bg-cyan-400 blur-2xl"
|
||||
></motion.div>
|
||||
<motion.div
|
||||
initial={{ width: "15rem" }}
|
||||
whileInView={{ width: "30rem" }}
|
||||
transition={{
|
||||
delay: 0.3,
|
||||
duration: 0.8,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
className="absolute inset-auto z-50 h-0.5 w-[30rem] -translate-y-[7rem] bg-cyan-400 "
|
||||
></motion.div>
|
||||
|
||||
<div className="absolute inset-auto z-40 h-44 w-full -translate-y-[12.5rem] bg-slate-950 "></div>
|
||||
</div>
|
||||
|
||||
<div className="relative z-50 flex -translate-y-80 flex-col items-center px-5">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
121
web/components/ui/navbar-menu.tsx
Normal file
|
@ -0,0 +1,121 @@
|
|||
"use client";
|
||||
import React from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
|
||||
const transition = {
|
||||
type: "spring",
|
||||
mass: 0.5,
|
||||
damping: 11.5,
|
||||
stiffness: 100,
|
||||
restDelta: 0.001,
|
||||
restSpeed: 0.001,
|
||||
};
|
||||
|
||||
export const MenuItem = ({
|
||||
setActive,
|
||||
active,
|
||||
item,
|
||||
children,
|
||||
}: {
|
||||
setActive: (item: string) => void;
|
||||
active: string | null;
|
||||
item: string;
|
||||
children?: React.ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<div onMouseEnter={() => setActive(item)} className="relative ">
|
||||
<motion.p
|
||||
transition={{ duration: 0.3 }}
|
||||
className="cursor-pointer text-black hover:opacity-[0.9] dark:text-white"
|
||||
>
|
||||
{item}
|
||||
</motion.p>
|
||||
{active !== null && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.85, y: 10 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
transition={transition}
|
||||
>
|
||||
{active === item && (
|
||||
<div className="absolute top-[calc(100%_+_1.2rem)] left-1/2 transform -translate-x-1/2 pt-4">
|
||||
<motion.div
|
||||
transition={transition}
|
||||
layoutId="active" // layoutId ensures smooth animation
|
||||
className="bg-white dark:bg-black backdrop-blur-sm rounded-2xl overflow-hidden border border-black/[0.2] dark:border-white/[0.2] shadow-xl"
|
||||
>
|
||||
<motion.div
|
||||
layout // layout ensures smooth animation
|
||||
className="w-max h-full p-4"
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Menu = ({
|
||||
setActive,
|
||||
children,
|
||||
}: {
|
||||
setActive: (item: string | null) => void;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<nav
|
||||
onMouseLeave={() => setActive(null)} // resets the state
|
||||
className="relative rounded-full border dark:bg-black/20 dark:border-white/[0.2] bg-white/20 shadow-input flex justify-center md:justify-between space-x-4 px-10 py-4 place-items-center backdrop-blur-lg"
|
||||
>
|
||||
{children}
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
export const ProductItem = ({
|
||||
title,
|
||||
description,
|
||||
href,
|
||||
src,
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
href: string;
|
||||
src: string;
|
||||
}) => {
|
||||
return (
|
||||
<Link href={href} className="flex space-x-2">
|
||||
<Image
|
||||
src={src}
|
||||
width={140}
|
||||
height={70}
|
||||
alt={title}
|
||||
className="flex-shrink-0 rounded-md shadow-2xl"
|
||||
/>
|
||||
<div>
|
||||
<h4 className="text-xl font-bold mb-1 text-black dark:text-white">
|
||||
{title}
|
||||
</h4>
|
||||
<p className="text-neutral-700 text-sm max-w-[10rem] dark:text-neutral-300">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export const HoveredLink = ({ children, ...rest }: any) => {
|
||||
return (
|
||||
<Link
|
||||
{...rest}
|
||||
className="text-neutral-700 dark:text-neutral-200 hover:text-black "
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
};
|
117
web/components/ui/table.tsx
Normal file
|
@ -0,0 +1,117 @@
|
|||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Table = React.forwardRef<
|
||||
HTMLTableElement,
|
||||
React.HTMLAttributes<HTMLTableElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className="relative w-full overflow-auto">
|
||||
<table
|
||||
ref={ref}
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
Table.displayName = "Table"
|
||||
|
||||
const TableHeader = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
||||
))
|
||||
TableHeader.displayName = "TableHeader"
|
||||
|
||||
const TableBody = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tbody
|
||||
ref={ref}
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableBody.displayName = "TableBody"
|
||||
|
||||
const TableFooter = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tfoot
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableFooter.displayName = "TableFooter"
|
||||
|
||||
const TableRow = React.forwardRef<
|
||||
HTMLTableRowElement,
|
||||
React.HTMLAttributes<HTMLTableRowElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableRow.displayName = "TableRow"
|
||||
|
||||
const TableHead = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<th
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableHead.displayName = "TableHead"
|
||||
|
||||
const TableCell = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<td
|
||||
ref={ref}
|
||||
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableCell.displayName = "TableCell"
|
||||
|
||||
const TableCaption = React.forwardRef<
|
||||
HTMLTableCaptionElement,
|
||||
React.HTMLAttributes<HTMLTableCaptionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<caption
|
||||
ref={ref}
|
||||
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableCaption.displayName = "TableCaption"
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
}
|
129
web/components/ui/toast.tsx
Normal file
|
@ -0,0 +1,129 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as ToastPrimitives from "@radix-ui/react-toast"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { X } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const ToastProvider = ToastPrimitives.Provider
|
||||
|
||||
const ToastViewport = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Viewport
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
|
||||
|
||||
const toastVariants = cva(
|
||||
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "border bg-background text-foreground",
|
||||
destructive:
|
||||
"destructive group border-destructive bg-destructive text-destructive-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const Toast = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
||||
VariantProps<typeof toastVariants>
|
||||
>(({ className, variant, ...props }, ref) => {
|
||||
return (
|
||||
<ToastPrimitives.Root
|
||||
ref={ref}
|
||||
className={cn(toastVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Toast.displayName = ToastPrimitives.Root.displayName
|
||||
|
||||
const ToastAction = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Action>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Action
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastAction.displayName = ToastPrimitives.Action.displayName
|
||||
|
||||
const ToastClose = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Close>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Close
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
|
||||
className
|
||||
)}
|
||||
toast-close=""
|
||||
{...props}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</ToastPrimitives.Close>
|
||||
))
|
||||
ToastClose.displayName = ToastPrimitives.Close.displayName
|
||||
|
||||
const ToastTitle = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Title
|
||||
ref={ref}
|
||||
className={cn("text-sm font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastTitle.displayName = ToastPrimitives.Title.displayName
|
||||
|
||||
const ToastDescription = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm opacity-90", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastDescription.displayName = ToastPrimitives.Description.displayName
|
||||
|
||||
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
|
||||
|
||||
type ToastActionElement = React.ReactElement<typeof ToastAction>
|
||||
|
||||
export {
|
||||
type ToastProps,
|
||||
type ToastActionElement,
|
||||
ToastProvider,
|
||||
ToastViewport,
|
||||
Toast,
|
||||
ToastTitle,
|
||||
ToastDescription,
|
||||
ToastClose,
|
||||
ToastAction,
|
||||
}
|
35
web/components/ui/toaster.tsx
Normal file
|
@ -0,0 +1,35 @@
|
|||
"use client"
|
||||
|
||||
import {
|
||||
Toast,
|
||||
ToastClose,
|
||||
ToastDescription,
|
||||
ToastProvider,
|
||||
ToastTitle,
|
||||
ToastViewport,
|
||||
} from "@/components/ui/toast"
|
||||
import { useToast } from "@/components/ui/use-toast"
|
||||
|
||||
export function Toaster() {
|
||||
const { toasts } = useToast()
|
||||
|
||||
return (
|
||||
<ToastProvider>
|
||||
{toasts.map(function ({ id, title, description, action, ...props }) {
|
||||
return (
|
||||
<Toast key={id} {...props}>
|
||||
<div className="grid gap-1">
|
||||
{title && <ToastTitle>{title}</ToastTitle>}
|
||||
{description && (
|
||||
<ToastDescription>{description}</ToastDescription>
|
||||
)}
|
||||
</div>
|
||||
{action}
|
||||
<ToastClose />
|
||||
</Toast>
|
||||
)
|
||||
})}
|
||||
<ToastViewport />
|
||||
</ToastProvider>
|
||||
)
|
||||
}
|
194
web/components/ui/use-toast.ts
Normal file
|
@ -0,0 +1,194 @@
|
|||
"use client"
|
||||
|
||||
// Inspired by react-hot-toast library
|
||||
import * as React from "react"
|
||||
|
||||
import type {
|
||||
ToastActionElement,
|
||||
ToastProps,
|
||||
} from "@/components/ui/toast"
|
||||
|
||||
const TOAST_LIMIT = 1
|
||||
const TOAST_REMOVE_DELAY = 1000000
|
||||
|
||||
type ToasterToast = ToastProps & {
|
||||
id: string
|
||||
title?: React.ReactNode
|
||||
description?: React.ReactNode
|
||||
action?: ToastActionElement
|
||||
}
|
||||
|
||||
const actionTypes = {
|
||||
ADD_TOAST: "ADD_TOAST",
|
||||
UPDATE_TOAST: "UPDATE_TOAST",
|
||||
DISMISS_TOAST: "DISMISS_TOAST",
|
||||
REMOVE_TOAST: "REMOVE_TOAST",
|
||||
} as const
|
||||
|
||||
let count = 0
|
||||
|
||||
function genId() {
|
||||
count = (count + 1) % Number.MAX_SAFE_INTEGER
|
||||
return count.toString()
|
||||
}
|
||||
|
||||
type ActionType = typeof actionTypes
|
||||
|
||||
type Action =
|
||||
| {
|
||||
type: ActionType["ADD_TOAST"]
|
||||
toast: ToasterToast
|
||||
}
|
||||
| {
|
||||
type: ActionType["UPDATE_TOAST"]
|
||||
toast: Partial<ToasterToast>
|
||||
}
|
||||
| {
|
||||
type: ActionType["DISMISS_TOAST"]
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
| {
|
||||
type: ActionType["REMOVE_TOAST"]
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
|
||||
interface State {
|
||||
toasts: ToasterToast[]
|
||||
}
|
||||
|
||||
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
|
||||
|
||||
const addToRemoveQueue = (toastId: string) => {
|
||||
if (toastTimeouts.has(toastId)) {
|
||||
return
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
toastTimeouts.delete(toastId)
|
||||
dispatch({
|
||||
type: "REMOVE_TOAST",
|
||||
toastId: toastId,
|
||||
})
|
||||
}, TOAST_REMOVE_DELAY)
|
||||
|
||||
toastTimeouts.set(toastId, timeout)
|
||||
}
|
||||
|
||||
export const reducer = (state: State, action: Action): State => {
|
||||
switch (action.type) {
|
||||
case "ADD_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||
}
|
||||
|
||||
case "UPDATE_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === action.toast.id ? { ...t, ...action.toast } : t
|
||||
),
|
||||
}
|
||||
|
||||
case "DISMISS_TOAST": {
|
||||
const { toastId } = action
|
||||
|
||||
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
||||
// but I'll keep it here for simplicity
|
||||
if (toastId) {
|
||||
addToRemoveQueue(toastId)
|
||||
} else {
|
||||
state.toasts.forEach((toast) => {
|
||||
addToRemoveQueue(toast.id)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === toastId || toastId === undefined
|
||||
? {
|
||||
...t,
|
||||
open: false,
|
||||
}
|
||||
: t
|
||||
),
|
||||
}
|
||||
}
|
||||
case "REMOVE_TOAST":
|
||||
if (action.toastId === undefined) {
|
||||
return {
|
||||
...state,
|
||||
toasts: [],
|
||||
}
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const listeners: Array<(state: State) => void> = []
|
||||
|
||||
let memoryState: State = { toasts: [] }
|
||||
|
||||
function dispatch(action: Action) {
|
||||
memoryState = reducer(memoryState, action)
|
||||
listeners.forEach((listener) => {
|
||||
listener(memoryState)
|
||||
})
|
||||
}
|
||||
|
||||
type Toast = Omit<ToasterToast, "id">
|
||||
|
||||
function toast({ ...props }: Toast) {
|
||||
const id = genId()
|
||||
|
||||
const update = (props: ToasterToast) =>
|
||||
dispatch({
|
||||
type: "UPDATE_TOAST",
|
||||
toast: { ...props, id },
|
||||
})
|
||||
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
|
||||
|
||||
dispatch({
|
||||
type: "ADD_TOAST",
|
||||
toast: {
|
||||
...props,
|
||||
id,
|
||||
open: true,
|
||||
onOpenChange: (open) => {
|
||||
if (!open) dismiss()
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
id: id,
|
||||
dismiss,
|
||||
update,
|
||||
}
|
||||
}
|
||||
|
||||
function useToast() {
|
||||
const [state, setState] = React.useState<State>(memoryState)
|
||||
|
||||
React.useEffect(() => {
|
||||
listeners.push(setState)
|
||||
return () => {
|
||||
const index = listeners.indexOf(setState)
|
||||
if (index > -1) {
|
||||
listeners.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}, [state])
|
||||
|
||||
return {
|
||||
...state,
|
||||
toast,
|
||||
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
||||
}
|
||||
}
|
||||
|
||||
export { useToast, toast }
|
6
web/lib/utils.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { type ClassValue, clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
4
web/next.config.mjs
Normal file
|
@ -0,0 +1,4 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {};
|
||||
|
||||
export default nextConfig;
|
41
web/package.json
Normal file
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"name": "web",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-collapsible": "^1.1.0",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@radix-ui/react-toast": "^1.2.1",
|
||||
"@types/react-google-recaptcha": "^2.1.9",
|
||||
"@uiw/react-markdown-preview": "^5.1.2",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"framer-motion": "^11.3.24",
|
||||
"lucide-react": "^0.426.0",
|
||||
"next": "14.2.5",
|
||||
"next-themes": "^0.3.0",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"react-element-to-jsx-string": "^15.0.0",
|
||||
"react-google-recaptcha": "^3.1.0",
|
||||
"react-markdown": "^9.0.1",
|
||||
"tailwind-merge": "^2.4.0",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.4.41",
|
||||
"tailwindcss": "^3.4.9",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
3428
web/pnpm-lock.yaml
generated
Normal file
8
web/postcss.config.mjs
Normal file
|
@ -0,0 +1,8 @@
|
|||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
BIN
web/public/SurfSense.png
Normal file
After Width: | Height: | Size: 68 KiB |
BIN
web/public/icon-128.png
Normal file
After Width: | Height: | Size: 5.5 KiB |
160
web/tailwind.config.ts
Normal file
|
@ -0,0 +1,160 @@
|
|||
import type { Config } from "tailwindcss";
|
||||
|
||||
const {
|
||||
default: flattenColorPalette,
|
||||
} = require("tailwindcss/lib/util/flattenColorPalette");
|
||||
|
||||
const config = {
|
||||
darkMode: ["class"],
|
||||
content: [
|
||||
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
],
|
||||
prefix: "",
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: "2rem",
|
||||
screens: {
|
||||
"2xl": "1400px",
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
background: "hsl(var(--background))",
|
||||
foreground: "hsl(var(--foreground))",
|
||||
primary: {
|
||||
DEFAULT: "hsl(var(--primary))",
|
||||
foreground: "hsl(var(--primary-foreground))",
|
||||
"50": "#eff6ff",
|
||||
"100": "#dbeafe",
|
||||
"200": "#bfdbfe",
|
||||
"300": "#93c5fd",
|
||||
"400": "#60a5fa",
|
||||
"500": "#3b82f6",
|
||||
"600": "#2563eb",
|
||||
"700": "#1d4ed8",
|
||||
"800": "#1e40af",
|
||||
"900": "#1e3a8a",
|
||||
"950": "#172554",
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "hsl(var(--secondary))",
|
||||
foreground: "hsl(var(--secondary-foreground))",
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "hsl(var(--destructive))",
|
||||
foreground: "hsl(var(--destructive-foreground))",
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsl(var(--muted))",
|
||||
foreground: "hsl(var(--muted-foreground))",
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "hsl(var(--accent))",
|
||||
foreground: "hsl(var(--accent-foreground))",
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: "hsl(var(--popover))",
|
||||
foreground: "hsl(var(--popover-foreground))",
|
||||
},
|
||||
card: {
|
||||
DEFAULT: "hsl(var(--card))",
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
},
|
||||
keyframes: {
|
||||
"accordion-down": {
|
||||
from: { height: "0" },
|
||||
to: { height: "var(--radix-accordion-content-height)" },
|
||||
},
|
||||
"accordion-up": {
|
||||
from: { height: "var(--radix-accordion-content-height)" },
|
||||
to: { height: "0" },
|
||||
},
|
||||
aurora: {
|
||||
from: {
|
||||
backgroundPosition: "50% 50%, 50% 50%",
|
||||
},
|
||||
to: {
|
||||
backgroundPosition: "350% 50%, 350% 50%",
|
||||
},
|
||||
},
|
||||
shimmer: {
|
||||
from: {
|
||||
backgroundPosition: "0 0",
|
||||
},
|
||||
to: {
|
||||
backgroundPosition: "-200% 0",
|
||||
},
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
aurora: "aurora 60s linear infinite",
|
||||
shimmer: "shimmer 2s linear infinite",
|
||||
},
|
||||
fontFamily: {
|
||||
'body': [
|
||||
'Inter',
|
||||
'ui-sans-serif',
|
||||
'system-ui',
|
||||
'-apple-system',
|
||||
'system-ui',
|
||||
'Segoe UI',
|
||||
'Roboto',
|
||||
'Helvetica Neue',
|
||||
'Arial',
|
||||
'Noto Sans',
|
||||
'sans-serif',
|
||||
'Apple Color Emoji',
|
||||
'Segoe UI Emoji',
|
||||
'Segoe UI Symbol',
|
||||
'Noto Color Emoji'
|
||||
],
|
||||
'sans': [
|
||||
'Inter',
|
||||
'ui-sans-serif',
|
||||
'system-ui',
|
||||
'-apple-system',
|
||||
'system-ui',
|
||||
'Segoe UI',
|
||||
'Roboto',
|
||||
'Helvetica Neue',
|
||||
'Arial',
|
||||
'Noto Sans',
|
||||
'sans-serif',
|
||||
'Apple Color Emoji',
|
||||
'Segoe UI Emoji',
|
||||
'Segoe UI Symbol',
|
||||
'Noto Color Emoji'
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: [require("tailwindcss-animate"), addVariablesForColors],
|
||||
} satisfies Config;
|
||||
|
||||
// This plugin adds each Tailwind color as a global CSS variable, e.g. var(--gray-200).
|
||||
function addVariablesForColors({ addBase, theme }: any) {
|
||||
let allColors = flattenColorPalette(theme("colors"));
|
||||
let newVars = Object.fromEntries(
|
||||
Object.entries(allColors).map(([key, val]) => [`--${key}`, val])
|
||||
);
|
||||
|
||||
addBase({
|
||||
":root": newVars,
|
||||
});
|
||||
}
|
||||
|
||||
export default config;
|
26
web/tsconfig.json
Normal file
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|